Анализ поведения пользователей мобильного приложения "Ненужные вещи"¶

Описание данных:

Датасет содержит данные о событиях, впервые совершивших действия в мобильном приложении "Ненужные вещи". В нем пользователи продают свои ненужные вещи, размещая их на доске объявлений.

В датасете mobile_sources.csv содержатся:

  • userId — идентификатор пользователя,
  • source — источник, с которого пользователь установил приложение.

В датасете mobile_dataset.csv содержатся:

  • event.time — время совершения,
  • user.id — идентификатор пользователя,
  • event.name — действие пользователя. Виды действий:
  1. advert_open — открыл карточки объявления,
  2. photos_show — просмотрел фотографий в объявлении,
  3. tips_show — увидел рекомендованные объявления,
  4. tips_click — кликнул по рекомендованному объявлению,
  5. contacts_show и show_contacts — посмотрел номер телефона,
  6. contacts_call — позвонил по номеру из объявления,
  7. map — открыл карту объявлений,
  8. search_1 — search_7 — разные действия, связанные с поиском по сайту,
  9. favorites_add — добавил объявление в избранное.

Цели исследования: Получить на основе поведения пользователей гипотезы о том как можно было бы улучшить приложение с точки зрения пользовательского опыта. Разбить пользователей на отдельные сегменты по поведению. Выделить основные сценарии (последовательности событий) использования приложения.\ Заказчик продакт менеджер, который занимается вовлеченностью пользователей. В дальнейшем заказчик будет проводить свои исследования на основе нашей сегментации.

Ход исследования:

  1. Загрузка данных: загрузка данных и изучение общей информации.
  • Импорт библиотек, настройка функций, отображений и загрузка датасетов
  • Изучение общей информации
  1. Предобработка данных: приведение названия столбцов к нижнему регистру, проверка пропусков, приведение данных к нужному типу.
  • Переименование названий столбцов
  • Приведение данных в нужный тип
  • Объединение действий пользователей
  • Промежуточный вывод
  1. Создание функций для расчетов: расписывание функций для создания данных.
  • Функция для создания пользовательских профилей - get_profiles()
  • Функция для расчёта удержания - get_retention()
  1. Исследовательский анализ данных: описание и визуализирование общей информации о пользователях.
  • Профили пользователей
  • Считаем Retention Rate
  • Визуализация когортного анализа
  1. Основные вопросы исследования:

5.1. Проанализируйте связь целевого события — просмотра контактов — и других действий пользователей.

  • В разрезе сессий отобрать сценарии\паттерны, которые приводят к просмотру контактов;
  • Построить воронки по основным сценариям в разрезе уникальных пользователей.

5.2. Оцените, какие действия чаще совершают те пользователи, которые просматривают контакты.

5.2.1 Рассчитать относительную частоту событий в разрезе двух групп пользователей:

  • группа пользователей, которые смотрели контакты contacts_show;
  • группа пользователей, которые не смотрели контакты contacts_show
  1. Проверка статистических гипотез
  • Гипотеза о различии конверсии в просмотры контактов (целевое действие) между двумя группами (group_1 - tips_show, tips_click и group_2 - tips_show)
  • Гипотеза о различии медианной длительности сессии между двумя группами (group_1 - достигшими целевого действия (event_name == contacts_show) и group_2 - не достигшими целевого действия (event_name != contacts_show))
  1. Общий вывод и рекомендации: резюмирование полученных результатов, формулировка ключевых выводов и рекомендаций.

С помощью данного исследования мы стремимся дать всестороннний анализ пользователей мобильного приложения "Ненужные вещи", что станет отправной точкой для дальнейшего развития компании.

Загрузка данных и изучение общей информации¶

Импорт библиотек, настройка функций, отображений и загрузка датасетов¶
In [1]:
# импортируем библиотеки pandas, matplot, numpy, scipy
import pandas as pd
import numpy as np
import seaborn as sns
import datetime as dt
import math as mth
from scipy import stats as st
from datetime import datetime, timedelta
from matplotlib import pyplot as plt

from plotly.subplots import make_subplots
import plotly.express as px
from plotly import graph_objects as go

import warnings
warnings.filterwarnings("ignore")

# устанавливаем отображение количества столбцов
pd.options.display.max_columns = 40

# устанавливаем отображение полного текста в ячейке
pd.set_option('display.max_colwidth', None)

# устанавливаем отображение чисел с двумя знаками после запятой
pd.set_option('display.float_format', '{:.2f}'.format)
In [2]:
# открываем файл
profiles = pd.read_csv('/Users/ildushisamov/Desktop/projects/final_project/mobile_app/mobile_sourсes.csv')
sessions = pd.read_csv('/Users/ildushisamov/Desktop/projects/final_project/mobile_app/mobile_dataset.csv')
In [3]:
# создадим универсальную функцию, которая будет принимать на вход датафрейм,
# а на выходе она будет выводить все нужные характеристики:

def my_func(x):
    print('-'*15, 'Исходный датафрейм', '-'*15)
    display(x.head())
    print('')
    print('')
    print('-'*15, 'Общая информация о датафрейме', '-'*15)
    print('')
    print('')
    x.info()
    print('-'*15, 'Количество пустых значений в датафрейме', '-'*15)
    print('')
    print('')
    display(x.isna().sum())
    print('-'*15, 'Количество явных дубликатов в датафрейме', '-'*15)
    display(x.duplicated().sum())
Изучение общей информации¶
In [4]:
my_func(profiles)
--------------- Исходный датафрейм ---------------
userId source
0 020292ab-89bc-4156-9acf-68bc2783f894 other
1 cf7eda61-9349-469f-ac27-e5b6f5ec475c yandex
2 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 yandex
3 d9b06b47-0f36-419b-bbb0-3533e582a6cb other
4 f32e1e2a-3027-4693-b793-b7b3ff274439 google

--------------- Общая информация о датафрейме ---------------


<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4293 entries, 0 to 4292
Data columns (total 2 columns):
 #   Column  Non-Null Count  Dtype 
---  ------  --------------  ----- 
 0   userId  4293 non-null   object
 1   source  4293 non-null   object
dtypes: object(2)
memory usage: 67.2+ KB
--------------- Количество пустых значений в датафрейме ---------------


userId    0
source    0
dtype: int64
--------------- Количество явных дубликатов в датафрейме ---------------
0
In [5]:
profiles.nunique()
Out[5]:
userId    4293
source       3
dtype: int64
In [6]:
profiles['source'].unique()
Out[6]:
array(['other', 'yandex', 'google'], dtype=object)

В датасете 4293 записей (вместе с шапкой таблицы) и 2 колонки:

  • userId — идентификатор пользователя, всего насчитывается 4293 уникальных пользователя,
  • source — источник, с которого пользователь установил приложение, всего есть 3 источника: other, yandex, google.

Предобработка:

  • Приведем название колонки в нижний регистр и поставим нижнее подчеркивание, вместо слитного написания;
  • Явных дубликатов нет;
  • Пропусков нет;
  • Типы данных менять не нужно.
In [7]:
my_func(sessions)
--------------- Исходный датафрейм ---------------
event.time event.name user.id
0 2019-10-07 00:00:00.431357 advert_open 020292ab-89bc-4156-9acf-68bc2783f894
1 2019-10-07 00:00:01.236320 tips_show 020292ab-89bc-4156-9acf-68bc2783f894
2 2019-10-07 00:00:02.245341 tips_show cf7eda61-9349-469f-ac27-e5b6f5ec475c
3 2019-10-07 00:00:07.039334 tips_show 020292ab-89bc-4156-9acf-68bc2783f894
4 2019-10-07 00:00:56.319813 advert_open cf7eda61-9349-469f-ac27-e5b6f5ec475c

--------------- Общая информация о датафрейме ---------------


<class 'pandas.core.frame.DataFrame'>
RangeIndex: 74197 entries, 0 to 74196
Data columns (total 3 columns):
 #   Column      Non-Null Count  Dtype 
---  ------      --------------  ----- 
 0   event.time  74197 non-null  object
 1   event.name  74197 non-null  object
 2   user.id     74197 non-null  object
dtypes: object(3)
memory usage: 1.7+ MB
--------------- Количество пустых значений в датафрейме ---------------


event.time    0
event.name    0
user.id       0
dtype: int64
--------------- Количество явных дубликатов в датафрейме ---------------
0
In [8]:
sessions.nunique()
Out[8]:
event.time    74197
event.name       16
user.id        4293
dtype: int64

В датасете mobile_dataset 74197 записей (вместе с шапкой таблицы) и 3 колонки:

  • event.time — время совершения действия, всего в датасете 74197 сессий,
  • user.id — идентификатор пользователя,
  • event.name — название действия пользователя, всего 16 уникальных действий пользователя.

Предобработка:

  • Поставим нижнее подчеркивание в названиях колонок, вместо точек и переименуем event.time на session_start;
  • Явных дубликатов нет;
  • Пропусков нет;
  • Поменяем тип данных в колонке event.time на datetime;
  • Объединим действия пользователей в группы.
In [9]:
sessions['event.name'].value_counts()
Out[9]:
event.name
tips_show        40055
photos_show      10012
advert_open       6164
contacts_show     4450
map               3881
search_1          3506
favorites_add     1417
search_5          1049
tips_click         814
search_4           701
contacts_call      541
search_3           522
search_6           460
search_2           324
search_7           222
show_contacts       79
Name: count, dtype: int64

Для удобства поиска сценариев стоит объединить действия пользователей:

  • search_1 - search_7 в search;
  • show_contacts в contacts_show.

Предобработка данных¶

Переименование названий столбцов¶
In [10]:
# поменяем названия столбцов
profiles.rename(columns = {'userId': 'user_id'}, inplace = True)

sessions.columns = sessions.columns.str.replace('.', '_')
sessions.rename(columns = {'event_time': 'session_start'}, inplace = True)

display(list(profiles))
list(sessions)
['user_id', 'source']
Out[10]:
['session_start', 'event_name', 'user_id']
Приведение данных в нужный тип¶
In [11]:
# поменяем типы данных
sessions['session_start'] = pd.to_datetime(sessions['session_start'])

sessions.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 74197 entries, 0 to 74196
Data columns (total 3 columns):
 #   Column         Non-Null Count  Dtype         
---  ------         --------------  -----         
 0   session_start  74197 non-null  datetime64[ns]
 1   event_name     74197 non-null  object        
 2   user_id        74197 non-null  object        
dtypes: datetime64[ns](1), object(2)
memory usage: 1.7+ MB

Добавим новый столбец день совершения определенного действия

In [12]:
sessions['dt'] = sessions['session_start'].dt.date

sessions
Out[12]:
session_start event_name user_id dt
0 2019-10-07 00:00:00.431357 advert_open 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07
1 2019-10-07 00:00:01.236320 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07
2 2019-10-07 00:00:02.245341 tips_show cf7eda61-9349-469f-ac27-e5b6f5ec475c 2019-10-07
3 2019-10-07 00:00:07.039334 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07
4 2019-10-07 00:00:56.319813 advert_open cf7eda61-9349-469f-ac27-e5b6f5ec475c 2019-10-07
... ... ... ... ...
74192 2019-11-03 23:53:29.534986 tips_show 28fccdf4-7b9e-42f5-bc73-439a265f20e9 2019-11-03
74193 2019-11-03 23:54:00.407086 tips_show 28fccdf4-7b9e-42f5-bc73-439a265f20e9 2019-11-03
74194 2019-11-03 23:56:57.041825 search_1 20850c8f-4135-4059-b13b-198d3ac59902 2019-11-03
74195 2019-11-03 23:57:06.232189 tips_show 28fccdf4-7b9e-42f5-bc73-439a265f20e9 2019-11-03
74196 2019-11-03 23:58:12.532487 tips_show 28fccdf4-7b9e-42f5-bc73-439a265f20e9 2019-11-03

74197 rows × 4 columns

Объединение действий пользователей¶
In [13]:
sessions['event_name'] = sessions['event_name'].str.replace('show_contacts', 'contacts_show')
sessions['event_name'] = sessions['event_name'].str.replace('^search_\d+', 'search', regex=True)

sessions['event_name'].value_counts()
Out[13]:
event_name
tips_show        40055
photos_show      10012
search            6784
advert_open       6164
contacts_show     4529
map               3881
favorites_add     1417
tips_click         814
contacts_call      541
Name: count, dtype: int64
Промежуточный вывод¶

В данном шаге мы проверили датасет на пропуски и дубликаты, привели данные в нужный тип, переименовали названия столбцов и объединили действия пользователей в группы.

После всех преобразований стоит проверить данные на наличие дубликатов. Этот шаг поможет обеспечить более точные результаты анализа.

In [14]:
my_func(profiles)
--------------- Исходный датафрейм ---------------
user_id source
0 020292ab-89bc-4156-9acf-68bc2783f894 other
1 cf7eda61-9349-469f-ac27-e5b6f5ec475c yandex
2 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 yandex
3 d9b06b47-0f36-419b-bbb0-3533e582a6cb other
4 f32e1e2a-3027-4693-b793-b7b3ff274439 google

--------------- Общая информация о датафрейме ---------------


<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4293 entries, 0 to 4292
Data columns (total 2 columns):
 #   Column   Non-Null Count  Dtype 
---  ------   --------------  ----- 
 0   user_id  4293 non-null   object
 1   source   4293 non-null   object
dtypes: object(2)
memory usage: 67.2+ KB
--------------- Количество пустых значений в датафрейме ---------------


user_id    0
source     0
dtype: int64
--------------- Количество явных дубликатов в датафрейме ---------------
0
In [15]:
my_func(sessions)
--------------- Исходный датафрейм ---------------
session_start event_name user_id dt
0 2019-10-07 00:00:00.431357 advert_open 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07
1 2019-10-07 00:00:01.236320 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07
2 2019-10-07 00:00:02.245341 tips_show cf7eda61-9349-469f-ac27-e5b6f5ec475c 2019-10-07
3 2019-10-07 00:00:07.039334 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07
4 2019-10-07 00:00:56.319813 advert_open cf7eda61-9349-469f-ac27-e5b6f5ec475c 2019-10-07

--------------- Общая информация о датафрейме ---------------


<class 'pandas.core.frame.DataFrame'>
RangeIndex: 74197 entries, 0 to 74196
Data columns (total 4 columns):
 #   Column         Non-Null Count  Dtype         
---  ------         --------------  -----         
 0   session_start  74197 non-null  datetime64[ns]
 1   event_name     74197 non-null  object        
 2   user_id        74197 non-null  object        
 3   dt             74197 non-null  object        
dtypes: datetime64[ns](1), object(3)
memory usage: 2.3+ MB
--------------- Количество пустых значений в датафрейме ---------------


session_start    0
event_name       0
user_id          0
dt               0
dtype: int64
--------------- Количество явных дубликатов в датафрейме ---------------
0

Дубликатов не обнаружено.

Создание функций для расчета и анализа удержания и конверсий¶

Это функции для вычисления значений метрик:

  • get_profiles() — для создания профилей пользователей,
  • get_retention() — для подсчёта Retention Rate.
Функция для создания пользовательских профилей - get_profiles()¶
In [16]:
def get_profiles(sessions):

    # сортируем сессии по ID пользователя и дате посещения
    # группируем по ID и находим первые значения session_start и event_name
    # столбец с временем первого посещения назовём first_ts
    profiles = (
        df.sort_values(by=['user_id', 'session_start'])
        .groupby(['user_id', 'dt'])
        .agg({'session_start': 'first', 'event_name': 'first'})
        .rename(columns={'session_start': 'first_ts', 'event_name': 'first_event'})
        .reset_index()  # возвращаем user_id из индекса
    )

    # определяем дату первого посещения
    # и первый день месяца, в который это посещение произошло
    # эти данные понадобятся для когортного анализа
    profiles['dt'] = profiles['first_ts'].dt.date
    
    return profiles
Функция для расчёта удержания - get_retention()¶
In [17]:
def get_retention(
    profiles, sessions, observation_date, horizon_days, ignore_horizon=False
):

    # исключаем пользователей, не «доживших» до горизонта анализа
    last_suitable_acquisition_date = observation_date
    if not ignore_horizon:
        last_suitable_acquisition_date = observation_date - timedelta(
            days=horizon_days - 1
        )
    result_raw = profiles.query('dt <= @last_suitable_acquisition_date')

    # собираем «сырые» данные для расчёта удержания
    result_raw = result_raw.merge(
        sessions[['user_id', 'session_start']], on='user_id', how='left'
    )
    
    # вычисляем для каждого действия число недели, в котором было совершено действие
    result_raw['dt_week'] = result_raw['first_ts'].dt.isocalendar().week
    
    # вычисляем лайфтайм для каждой сессии в днях
    result_raw['lifetime'] = (
        result_raw['session_start'] - result_raw['first_ts']
    ).dt.days

    # создадим функцию для разделения дней лайфтаймов на недели
    # 0-6 день - 1 неделя;
    # 7-13 день - 2 неделя;
    # 14-20 день - 3 неделя;
    # 21-27 день - 4 неделя

    def replace(lifetime):
        if lifetime <= 6:
            return 0
        elif lifetime > 6 and lifetime <= 13:
            return 1
        elif lifetime > 13 and lifetime <= 20:
            return 2
        else:
            return 3
        
    result_raw['lifetime_week'] = result_raw['lifetime'].apply(replace)
    
    # рассчитываем удержание
    result_grouped = result_raw.pivot_table(
        index=['dt_week'], columns='lifetime_week', values='user_id', aggfunc='nunique'
    )
    cohort_sizes = (
        result_raw.groupby('dt_week')
        .agg({'user_id': 'nunique'})
        .rename(columns={'user_id': 'cohort_size'})
    )
    result_grouped = cohort_sizes.merge(
        result_grouped, on='dt_week', how='left'
    ).fillna(0)
    result_grouped = result_grouped.div(result_grouped['cohort_size'], axis=0)

    # исключаем все лайфтаймы, превышающие горизонт анализа
    result_grouped = result_grouped[
        ['cohort_size'] + list(range(horizon_days))
    ]

    # восстанавливаем столбец с размерами когорт
    result_grouped['cohort_size'] = cohort_sizes

    # возвращаем таблицу удержания и сырые данные
    return result_raw, result_grouped

Исследовательский анализ данных¶

In [18]:
# соединяем 2 датасета
df = pd.merge(sessions, profiles, left_on='user_id', right_on='user_id')
df
Out[18]:
session_start event_name user_id dt source
0 2019-10-07 00:00:00.431357 advert_open 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
1 2019-10-07 00:00:01.236320 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
2 2019-10-07 00:00:07.039334 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
3 2019-10-07 00:01:27.770232 advert_open 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
4 2019-10-07 00:01:34.804591 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
... ... ... ... ... ...
74192 2019-11-03 23:46:47.068179 map d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google
74193 2019-11-03 23:46:58.914787 advert_open d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google
74194 2019-11-03 23:47:01.232230 tips_show d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google
74195 2019-11-03 23:47:47.475102 advert_open d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google
74196 2019-11-03 23:47:50.087645 tips_show d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google

74197 rows × 5 columns

Профили пользователей¶
In [19]:
# создаем профиль пользователей
profiles = get_profiles(sessions)
profiles
Out[19]:
user_id dt first_ts first_event
0 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 2019-10-07 13:39:45.989359 tips_show
1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-09 2019-10-09 18:33:55.577963 map
2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-21 2019-10-21 19:52:30.778932 tips_show
3 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-22 2019-10-22 11:18:14.635436 map
4 00157779-810c-4498-9e05-a1e9e3cedf93 2019-10-19 2019-10-19 21:34:33.849769 search
... ... ... ... ...
7812 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-10-29 2019-10-29 13:58:47.865084 tips_show
7813 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-10-30 2019-10-30 00:15:43.363752 contacts_show
7814 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-01 2019-11-01 00:24:31.162871 tips_show
7815 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-02 2019-11-02 01:16:48.947231 tips_show
7816 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 2019-11-03 14:32:55.956301 tips_show

7817 rows × 4 columns

In [20]:
# посмотрим количество уникальных пользователей по первым действиям
(profiles
 .groupby('first_event')
 .agg({'user_id': 'nunique'})
 .sort_values(by='user_id', ascending=False)
 .reset_index()
)
Out[20]:
first_event user_id
0 tips_show 1646
1 search 1343
2 map 908
3 photos_show 671
4 contacts_show 303
5 advert_open 204
6 favorites_add 71
7 tips_click 20
In [21]:
# построим график, отображающий динамику пользователей по каждому из первых действий
profiles.pivot_table(
    index='dt',  # даты первых посещений
    columns='first_event',  # первые действия
    values='user_id',  # ID пользователей
    aggfunc='nunique'  # подсчёт уникальных значений
).plot(figsize=(15, 5), grid=True)

plt.title('Диаграмма динамики пользователей по каждому из первых действий')
plt.xlabel('Дата')
plt.ylabel('Количество уникальных пользователей в день')
plt.show()

В течение всех 4 недель первые действия у пользователей были tips_show, т.е. первое и самое частое было то, что пользователи видели рекомендованное объявление, на втором месте находится search - пользователь искал интересное ему объявление.

In [22]:
# создаем профиль пользователей с последним действием
profiles_last = (
        df.sort_values(by=['user_id', 'session_start'])
        .groupby(['user_id', 'dt'])
        .agg({'event_name': 'last'})
        .rename(columns={'dt': 'last_dt', 'event_name': 'last_event'})
        .reset_index()
    )

profiles_last
Out[22]:
user_id dt last_event
0 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 tips_show
1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-09 tips_show
2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-21 tips_show
3 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-22 tips_show
4 00157779-810c-4498-9e05-a1e9e3cedf93 2019-10-19 photos_show
... ... ... ...
7812 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-10-29 contacts_show
7813 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-10-30 tips_show
7814 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-01 tips_show
7815 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-02 tips_show
7816 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 tips_show

7817 rows × 3 columns

In [23]:
# посмотрим количество уникальных пользователей по последним действиям
(profiles_last
 .groupby('last_event')
 .agg({'user_id': 'nunique'})
 .sort_values(by='user_id', ascending=False)
 .reset_index()
)
Out[23]:
last_event user_id
0 tips_show 2558
1 photos_show 882
2 search 503
3 contacts_show 419
4 map 224
5 advert_open 211
6 favorites_add 134
7 contacts_call 133
8 tips_click 34
In [24]:
# построим график, отображающий динамику пользователей по каждому из последних действий
profiles_last.pivot_table(
    index='dt',  # даты первых посещений
    columns='last_event',  # источники переходов
    values='user_id',  # ID пользователей
    aggfunc='nunique'  # подсчёт уникальных значений
).plot(figsize=(15, 5), grid=True)

plt.title('Диаграмма динамики пользователей по каждому из последних действий')
plt.xlabel('Дата')
plt.ylabel('Количество уникальных пользователей в день')
plt.show()

Самым частым последним действием пользователей за 4 недели, было тоже самое действие - tips_show, на втором месте находится photos_show - пользователи просматривали фото в объявлении.

In [25]:
# присоединяем session_start и event_name к профилям по столбцу user_id и dt,
# чтобы добавить event_name к каждой сессии

result_raw = profiles.merge(
    sessions[['user_id', 'session_start', 'event_name', 'dt']], on=['user_id', 'dt'], how='left'
).reset_index().sort_values(['user_id', 'session_start'])

result_raw['session_dt'] = result_raw['session_start'].dt.date

# вычисляем для каждого действия число недели, в котором было совершено действие
result_raw['dt_week'] = result_raw['first_ts'].dt.isocalendar().week
result_raw
Out[25]:
index user_id dt first_ts first_event session_start event_name session_dt dt_week
0 0 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 2019-10-07 13:39:45.989359 tips_show 2019-10-07 13:39:45.989359 tips_show 2019-10-07 41
1 1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 2019-10-07 13:39:45.989359 tips_show 2019-10-07 13:40:31.052909 tips_show 2019-10-07 41
2 2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 2019-10-07 13:39:45.989359 tips_show 2019-10-07 13:41:05.722489 tips_show 2019-10-07 41
3 3 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 2019-10-07 13:39:45.989359 tips_show 2019-10-07 13:43:20.735461 tips_show 2019-10-07 41
4 4 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 2019-10-07 13:39:45.989359 tips_show 2019-10-07 13:45:30.917502 tips_show 2019-10-07 41
... ... ... ... ... ... ... ... ... ...
74192 74192 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 2019-11-03 14:32:55.956301 tips_show 2019-11-03 15:51:23.959572 tips_show 2019-11-03 44
74193 74193 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 2019-11-03 14:32:55.956301 tips_show 2019-11-03 15:51:57.899997 contacts_show 2019-11-03 44
74194 74194 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 2019-11-03 14:32:55.956301 tips_show 2019-11-03 16:07:40.932077 tips_show 2019-11-03 44
74195 74195 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 2019-11-03 14:32:55.956301 tips_show 2019-11-03 16:08:18.202734 tips_show 2019-11-03 44
74196 74196 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 2019-11-03 14:32:55.956301 tips_show 2019-11-03 16:08:25.388712 tips_show 2019-11-03 44

74197 rows × 9 columns

In [26]:
# посмотрим общую информацию по таблице
result_raw.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 74197 entries, 0 to 74196
Data columns (total 9 columns):
 #   Column         Non-Null Count  Dtype         
---  ------         --------------  -----         
 0   index          74197 non-null  int64         
 1   user_id        74197 non-null  object        
 2   dt             74197 non-null  object        
 3   first_ts       74197 non-null  datetime64[ns]
 4   first_event    74197 non-null  object        
 5   session_start  74197 non-null  datetime64[ns]
 6   event_name     74197 non-null  object        
 7   session_dt     74197 non-null  object        
 8   dt_week        74197 non-null  UInt32        
dtypes: UInt32(1), datetime64[ns](2), int64(1), object(5)
memory usage: 4.9+ MB
In [27]:
# посмотрим количество сессий по действиям
(result_raw
 .groupby('event_name')
 .agg({'session_start': 'count'})
 .sort_values(by='session_start', ascending=False)
)
Out[27]:
session_start
event_name
tips_show 40055
photos_show 10012
search 6784
advert_open 6164
contacts_show 4529
map 3881
favorites_add 1417
tips_click 814
contacts_call 541

Самым частым действием за 4 недели является также tips_show (40 тыс. действий) оно идёт в отрыве х4 от действия photos_show (10 тыс. действий), которое находится на 2 месте. Можно сказать, что люди которые пользуются приложением, только и видят рекомендованные объявления, которые отображаются автоматически и не зависят от их действий. Для некоторых расчетов в исследованиях это события учитывать не нужно, поэтому сделаем отдельный датасет с событиями без tips_show (event_name != tips_show).

Также важно отметить тот факт, что кликов по этим самым рекомендованным объявлениям всего - 814 совершенных действий. Подсчёт конверсий будем производить чуть позже.

In [28]:
result_raw['dt_week'].sort_values().unique()
Out[28]:
<IntegerArray>
[41, 42, 43, 44]
Length: 4, dtype: UInt32

Получилось 4 недели.

Считаем Retention Rate¶
In [29]:
print('Минимальная дата совершения действия:', df['dt'].unique().min())
print()
print('Максимальная дата совершения действия:', df['dt'].unique().max())
Минимальная дата совершения действия: 2019-10-07

Максимальная дата совершения действия: 2019-11-03

Данные компании выгружаются ежемесячно. Значит моментом анализа является - максимальная дата совершения действия, а горизонтом анализа оптимальней и удобней всего для анализа будет поставить промежуток времени - неделю. Удобней и для прогнозирования рекламных расходов в будущем, и для подсчетов Retention и Conversion Rate.

Также при горизонте анализа в неделю датасет разделится ровно на 4 недели.

In [30]:
# задаём момент и горизонт анализа данных
observation_date = datetime(2019, 11, 3).date()
horizon_weeks = 4

# создаём опцию «игнорировать горизонт»
ignore_horizon = False
In [31]:
retention_raw, retention = get_retention(
    profiles, sessions, observation_date, horizon_weeks
)

retention
Out[31]:
cohort_size 0 1 2 3
dt_week
41 1130 1.00 0.19 0.13 0.07
42 1438 1.00 0.21 0.10 0.00
43 1546 1.00 0.15 0.00 0.00
44 962 1.00 0.00 0.00 0.00
Визуализация когортного анализа¶
In [32]:
# строим хитмэп

plt.figure(figsize=(14, 10))  # задаём размер графика
sns.heatmap(
    retention.drop(columns=['cohort_size']),  # удаляем размеры когорт
    annot=True,  # включаем подписи
    fmt='.2%', # переводим значения в проценты
    cmap= 'coolwarm',
    yticklabels= ['1', '2', '3', '4'],
    xticklabels= ['1', '2', '3', '4']
)
plt.xlabel('Неделя лайфтаймов')
plt.ylabel('Неделя привлечения')
plt.title('Тепловая карта удержания')  # название графика
plt.show()

RR второй недели лайфтайма пришедших в 1 и 2 неделю (19,12% и 20,93%) заметно выше, по сравнению с пришедшими в 3 неделю (15,33%).

RR третьей недели лайфтайма выглядит заметно лучше у когорты пришедшей в 1 неделю (12,65%), чем у когорты пришедшей во неделе (9,67%).

Хочу отметить, что данный рассматриваемый промежуток очень короткий, и нужно больше времени, чтобы сделать достоверные выводы. Но если делать промежуточный вывод с теми данными, которые мы сейчас имеем, то можно сказать, что когорта пришедшая в 1 неделю выглядит уверенней всех остальных, хотя RR второй недели был выше у когорты пришедшей во вторую неделю. Привлеченные пользователи показывают RR в первую неделю лайфтайма от 15 до 21% удерживания пользователей, в третью неделю от 9,5 до 12,5%, а в четвертую неделю - 7.17% удержания.

Таким образом, стоит рассмотреть каналы привлечения пользователей, рассчитать RR ещё раз уже с более длинным диапазоном времени и понять какие каналы привлечения более привлекательны в рамках расчетов удержания пользователей.

Основные вопросы исследования¶

Выделение сессий¶

Для простоты расчета буду использовать календарный день за одну сессию.

Сессия = Календарный день

In [33]:
result_raw
Out[33]:
index user_id dt first_ts first_event session_start event_name session_dt dt_week
0 0 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 2019-10-07 13:39:45.989359 tips_show 2019-10-07 13:39:45.989359 tips_show 2019-10-07 41
1 1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 2019-10-07 13:39:45.989359 tips_show 2019-10-07 13:40:31.052909 tips_show 2019-10-07 41
2 2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 2019-10-07 13:39:45.989359 tips_show 2019-10-07 13:41:05.722489 tips_show 2019-10-07 41
3 3 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 2019-10-07 13:39:45.989359 tips_show 2019-10-07 13:43:20.735461 tips_show 2019-10-07 41
4 4 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 2019-10-07 13:39:45.989359 tips_show 2019-10-07 13:45:30.917502 tips_show 2019-10-07 41
... ... ... ... ... ... ... ... ... ...
74192 74192 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 2019-11-03 14:32:55.956301 tips_show 2019-11-03 15:51:23.959572 tips_show 2019-11-03 44
74193 74193 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 2019-11-03 14:32:55.956301 tips_show 2019-11-03 15:51:57.899997 contacts_show 2019-11-03 44
74194 74194 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 2019-11-03 14:32:55.956301 tips_show 2019-11-03 16:07:40.932077 tips_show 2019-11-03 44
74195 74195 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 2019-11-03 14:32:55.956301 tips_show 2019-11-03 16:08:18.202734 tips_show 2019-11-03 44
74196 74196 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 2019-11-03 14:32:55.956301 tips_show 2019-11-03 16:08:25.388712 tips_show 2019-11-03 44

74197 rows × 9 columns

result_raw_without_tips_show = result_raw.query('event_name != "tips_show"') result_raw_without_tips_show

In [34]:
profiles
Out[34]:
user_id dt first_ts first_event
0 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 2019-10-07 13:39:45.989359 tips_show
1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-09 2019-10-09 18:33:55.577963 map
2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-21 2019-10-21 19:52:30.778932 tips_show
3 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-22 2019-10-22 11:18:14.635436 map
4 00157779-810c-4498-9e05-a1e9e3cedf93 2019-10-19 2019-10-19 21:34:33.849769 search
... ... ... ... ...
7812 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-10-29 2019-10-29 13:58:47.865084 tips_show
7813 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-10-30 2019-10-30 00:15:43.363752 contacts_show
7814 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-01 2019-11-01 00:24:31.162871 tips_show
7815 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-02 2019-11-02 01:16:48.947231 tips_show
7816 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 2019-11-03 14:32:55.956301 tips_show

7817 rows × 4 columns

In [35]:
# группируем датасет по session_dt, user_id, first_event
# тем самым получаем количество действий пользователя в день,
# а индексы будут означать идентификатор каждой сессии, их переименуем в session_id

final_df = result_raw.pivot_table(
index = ['session_dt', 'user_id', 'first_event'],
values = 'index',
aggfunc = 'count'
).reset_index().rename(columns = {'index': 'cnt_sessions'})

final_df = final_df.reset_index().rename(columns = {'index': 'session_id'})
final_df
Out[35]:
session_id session_dt user_id first_event cnt_sessions
0 0 2019-10-07 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 tips_show 9
1 1 2019-10-07 00e714c6-66b9-4091-bce0-8589bd65b77c search 2
2 2 2019-10-07 020292ab-89bc-4156-9acf-68bc2783f894 advert_open 28
3 3 2019-10-07 024828cf-c873-43e6-8c7e-96aeb348699e tips_show 2
4 4 2019-10-07 032065eb-5bd5-4c5c-a4df-c60aa44c1e29 search 15
... ... ... ... ... ...
7812 7812 2019-11-03 f73fb5e2-7b99-45a0-bffa-29ff320399c1 search 9
7813 7813 2019-11-03 f8cfaf96-f241-4da7-ae8a-618aaf3a7fee tips_show 6
7814 7814 2019-11-03 f9b53876-74f9-45ea-b83d-b5722e0172af tips_show 4
7815 7815 2019-11-03 f9c19253-73e7-4b9e-9630-45353a792248 search 3
7816 7816 2019-11-03 fffb9e79-b927-4dbb-9b48-7fd09b23a62b tips_show 29

7817 rows × 5 columns

In [36]:
# далее присоединяем к прошлой таблице наш датасет, чтобы дальше можно было сгруппировать по event_name
# избавимся от повторяющихся событий в рамках сессии и оставляем последние действия

final_df = final_df.merge(
    result_raw, on = ['user_id', 'session_dt', 'first_event'], how = 'left'
).drop_duplicates(subset = ['session_id', 'event_name'], keep='last').reset_index(drop=True)

final_df
Out[36]:
session_id session_dt user_id first_event cnt_sessions index dt first_ts session_start event_name dt_week
0 0 2019-10-07 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 tips_show 9 8 2019-10-07 2019-10-07 13:39:45.989359 2019-10-07 13:49:41.716617 tips_show 41
1 1 2019-10-07 00e714c6-66b9-4091-bce0-8589bd65b77c search 2 270 2019-10-07 2019-10-07 22:00:35.765785 2019-10-07 22:01:43.074554 search 41
2 2 2019-10-07 020292ab-89bc-4156-9acf-68bc2783f894 advert_open 28 521 2019-10-07 2019-10-07 00:00:00.431357 2019-10-07 00:15:47.030323 map 41
3 2 2019-10-07 020292ab-89bc-4156-9acf-68bc2783f894 advert_open 28 523 2019-10-07 2019-10-07 00:00:00.431357 2019-10-07 00:18:37.324815 advert_open 41
4 2 2019-10-07 020292ab-89bc-4156-9acf-68bc2783f894 advert_open 28 524 2019-10-07 2019-10-07 00:00:00.431357 2019-10-07 00:18:42.917148 tips_show 41
... ... ... ... ... ... ... ... ... ... ... ...
14769 7815 2019-11-03 f9c19253-73e7-4b9e-9630-45353a792248 search 3 72179 2019-11-03 2019-11-03 11:38:41.424157 2019-11-03 11:38:41.424157 search 44
14770 7815 2019-11-03 f9c19253-73e7-4b9e-9630-45353a792248 search 3 72180 2019-11-03 2019-11-03 11:38:41.424157 2019-11-03 11:39:32.218473 contacts_show 44
14771 7815 2019-11-03 f9c19253-73e7-4b9e-9630-45353a792248 search 3 72181 2019-11-03 2019-11-03 11:38:41.424157 2019-11-03 11:39:43.578085 contacts_call 44
14772 7816 2019-11-03 fffb9e79-b927-4dbb-9b48-7fd09b23a62b tips_show 29 74193 2019-11-03 2019-11-03 14:32:55.956301 2019-11-03 15:51:57.899997 contacts_show 44
14773 7816 2019-11-03 fffb9e79-b927-4dbb-9b48-7fd09b23a62b tips_show 29 74196 2019-11-03 2019-11-03 14:32:55.956301 2019-11-03 16:08:25.388712 tips_show 44

14774 rows × 11 columns

Поиск сценариев¶

In [37]:
# выведем все сценарии для каждой сессии:
# сгруппируем по session_id и добавим вывод сценариев с функцией лямбда и добавлением действий поочердно в кортеж

scenarios = (final_df
             .groupby('session_id')
             .agg({'event_name':lambda x: tuple(x)})
             .reset_index()
             .rename(columns = {'event_name': 'scenarios'})
            )
scenarios
Out[37]:
session_id scenarios
0 0 (tips_show,)
1 1 (search,)
2 2 (map, advert_open, tips_show)
3 3 (tips_show,)
4 4 (search, tips_show)
... ... ...
7812 7812 (search, contacts_show, tips_show)
7813 7813 (tips_show, advert_open)
7814 7814 (tips_show,)
7815 7815 (search, contacts_show, contacts_call)
7816 7816 (contacts_show, tips_show)

7817 rows × 2 columns

In [38]:
# посчитаем количество уникальных сценарий

scenarios['scenarios'].nunique()
Out[38]:
412

Всего 412 уникальных сценарий.

Для некоторых задач требуется собрать датасет без событий tips_show, т.к. это действие не является показателем поведения пользователей.

Проанализируйте связь целевого события — просмотра контактов — и других действий пользователей.¶
В разрезе сессий отобрать сценарии\паттерны, которые приводят к просмотру контактов;¶

Для решения этой задачи нужно оставить только те сценарии, в которых последнее действие - целевое событие (contacts_show).

In [39]:
df
Out[39]:
session_start event_name user_id dt source
0 2019-10-07 00:00:00.431357 advert_open 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
1 2019-10-07 00:00:01.236320 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
2 2019-10-07 00:00:07.039334 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
3 2019-10-07 00:01:27.770232 advert_open 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
4 2019-10-07 00:01:34.804591 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
... ... ... ... ... ...
74192 2019-11-03 23:46:47.068179 map d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google
74193 2019-11-03 23:46:58.914787 advert_open d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google
74194 2019-11-03 23:47:01.232230 tips_show d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google
74195 2019-11-03 23:47:47.475102 advert_open d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google
74196 2019-11-03 23:47:50.087645 tips_show d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google

74197 rows × 5 columns

df_without_tips_show = df.query('event_name != "tips_show"') df_without_tips_show

In [40]:
# создаем профиль пользователей с последним действием
profiles_last = (
        df.sort_values(by=['user_id', 'session_start'])
        .groupby(['user_id', 'dt'])
        .agg({'event_name': 'last'})
        .rename(columns={'event_name': 'last_event', 'dt': 'last_dt'})
        .reset_index()
    )

profiles_last
Out[40]:
user_id dt last_event
0 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 tips_show
1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-09 tips_show
2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-21 tips_show
3 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-22 tips_show
4 00157779-810c-4498-9e05-a1e9e3cedf93 2019-10-19 photos_show
... ... ... ...
7812 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-10-29 contacts_show
7813 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-10-30 tips_show
7814 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-01 tips_show
7815 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-02 tips_show
7816 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 tips_show

7817 rows × 3 columns

In [41]:
# соединим таблицы, чтобы получить к каждому last_event - событие с event_name
result_raw_last = profiles_last.merge(
    sessions[['user_id', 'session_start', 'event_name', 'dt']], on=['user_id', 'dt'], how='left'
).reset_index().sort_values(['user_id', 'session_start'])

result_raw_last['session_dt'] = result_raw_last['session_start'].dt.date

result_raw_last
Out[41]:
index user_id dt last_event session_start event_name session_dt
0 0 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 tips_show 2019-10-07 13:39:45.989359 tips_show 2019-10-07
1 1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 tips_show 2019-10-07 13:40:31.052909 tips_show 2019-10-07
2 2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 tips_show 2019-10-07 13:41:05.722489 tips_show 2019-10-07
3 3 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 tips_show 2019-10-07 13:43:20.735461 tips_show 2019-10-07
4 4 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 tips_show 2019-10-07 13:45:30.917502 tips_show 2019-10-07
... ... ... ... ... ... ... ...
74192 74192 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 tips_show 2019-11-03 15:51:23.959572 tips_show 2019-11-03
74193 74193 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 tips_show 2019-11-03 15:51:57.899997 contacts_show 2019-11-03
74194 74194 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 tips_show 2019-11-03 16:07:40.932077 tips_show 2019-11-03
74195 74195 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 tips_show 2019-11-03 16:08:18.202734 tips_show 2019-11-03
74196 74196 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 tips_show 2019-11-03 16:08:25.388712 tips_show 2019-11-03

74197 rows × 7 columns

In [42]:
# группируем датасет по session_dt, user_id, last_event
# тем самым получаем количество действий пользователя в день
# а индексы будут означать session_id каждой сессии, их переименуем в session_id

final_df_last = result_raw_last.pivot_table(
index = ['session_dt', 'user_id', 'last_event'],
values = 'index',
aggfunc = 'count'
).reset_index().rename(columns = {'index': 'cnt_sessions'})

final_df_last = final_df_last.reset_index().rename(columns = {'index': 'session_id'})
final_df_last
#final_df_last.query('user_id == "00157779-810c-4498-9e05-a1e9e3cedf93"')
Out[42]:
session_id session_dt user_id last_event cnt_sessions
0 0 2019-10-07 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 tips_show 9
1 1 2019-10-07 00e714c6-66b9-4091-bce0-8589bd65b77c search 2
2 2 2019-10-07 020292ab-89bc-4156-9acf-68bc2783f894 tips_show 28
3 3 2019-10-07 024828cf-c873-43e6-8c7e-96aeb348699e tips_show 2
4 4 2019-10-07 032065eb-5bd5-4c5c-a4df-c60aa44c1e29 tips_show 15
... ... ... ... ... ...
7812 7812 2019-11-03 f73fb5e2-7b99-45a0-bffa-29ff320399c1 tips_show 9
7813 7813 2019-11-03 f8cfaf96-f241-4da7-ae8a-618aaf3a7fee advert_open 6
7814 7814 2019-11-03 f9b53876-74f9-45ea-b83d-b5722e0172af tips_show 4
7815 7815 2019-11-03 f9c19253-73e7-4b9e-9630-45353a792248 contacts_call 3
7816 7816 2019-11-03 fffb9e79-b927-4dbb-9b48-7fd09b23a62b tips_show 29

7817 rows × 5 columns

In [43]:
# добавим к нашей таблице колонку session_id
final_df = result_raw_last.merge(
    final_df_last,
    on=['session_dt', 'user_id', 'last_event'],
    how='left')

# добавим к нашей таблице колонку scenarios
final_df = final_df.merge(scenarios, on='session_id', how='left')
final_df
Out[43]:
index user_id dt last_event session_start event_name session_dt session_id cnt_sessions scenarios
0 0 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 tips_show 2019-10-07 13:39:45.989359 tips_show 2019-10-07 0 9 (tips_show,)
1 1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 tips_show 2019-10-07 13:40:31.052909 tips_show 2019-10-07 0 9 (tips_show,)
2 2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 tips_show 2019-10-07 13:41:05.722489 tips_show 2019-10-07 0 9 (tips_show,)
3 3 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 tips_show 2019-10-07 13:43:20.735461 tips_show 2019-10-07 0 9 (tips_show,)
4 4 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 tips_show 2019-10-07 13:45:30.917502 tips_show 2019-10-07 0 9 (tips_show,)
... ... ... ... ... ... ... ... ... ... ...
74192 74192 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 tips_show 2019-11-03 15:51:23.959572 tips_show 2019-11-03 7816 29 (contacts_show, tips_show)
74193 74193 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 tips_show 2019-11-03 15:51:57.899997 contacts_show 2019-11-03 7816 29 (contacts_show, tips_show)
74194 74194 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 tips_show 2019-11-03 16:07:40.932077 tips_show 2019-11-03 7816 29 (contacts_show, tips_show)
74195 74195 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 tips_show 2019-11-03 16:08:18.202734 tips_show 2019-11-03 7816 29 (contacts_show, tips_show)
74196 74196 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 tips_show 2019-11-03 16:08:25.388712 tips_show 2019-11-03 7816 29 (contacts_show, tips_show)

74197 rows × 10 columns

In [44]:
# оставим в датасете только те данные, где последнее действие - contacts_show
contacts_show = final_df.query('last_event == "contacts_show"')
contacts_show
Out[44]:
index user_id dt last_event session_start event_name session_dt session_id cnt_sessions scenarios
91 91 00157779-810c-4498-9e05-a1e9e3cedf93 2019-10-30 contacts_show 2019-10-30 07:50:45.948358 search 2019-10-30 6375 14 (photos_show, search, contacts_show)
92 92 00157779-810c-4498-9e05-a1e9e3cedf93 2019-10-30 contacts_show 2019-10-30 07:53:12.730053 photos_show 2019-10-30 6375 14 (photos_show, search, contacts_show)
93 93 00157779-810c-4498-9e05-a1e9e3cedf93 2019-10-30 contacts_show 2019-10-30 07:54:25.826815 photos_show 2019-10-30 6375 14 (photos_show, search, contacts_show)
94 94 00157779-810c-4498-9e05-a1e9e3cedf93 2019-10-30 contacts_show 2019-10-30 07:55:09.436282 search 2019-10-30 6375 14 (photos_show, search, contacts_show)
95 95 00157779-810c-4498-9e05-a1e9e3cedf93 2019-10-30 contacts_show 2019-10-30 07:57:00.426097 photos_show 2019-10-30 6375 14 (photos_show, search, contacts_show)
... ... ... ... ... ... ... ... ... ... ...
74151 74151 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-10-29 contacts_show 2019-10-29 15:19:08.638211 tips_show 2019-10-29 6374 20 (tips_show, contacts_show)
74152 74152 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-10-29 contacts_show 2019-10-29 15:19:10.766992 tips_show 2019-10-29 6374 20 (tips_show, contacts_show)
74153 74153 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-10-29 contacts_show 2019-10-29 16:12:15.417343 tips_show 2019-10-29 6374 20 (tips_show, contacts_show)
74154 74154 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-10-29 contacts_show 2019-10-29 16:12:17.466193 tips_show 2019-10-29 6374 20 (tips_show, contacts_show)
74155 74155 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-10-29 contacts_show 2019-10-29 16:13:00.681459 contacts_show 2019-10-29 6374 20 (tips_show, contacts_show)

5012 rows × 10 columns

In [45]:
(contacts_show
 .groupby('scenarios')
 .agg({'session_id': 'nunique'})
 .sort_values(by='session_id', ascending=False)
).head(20)
Out[45]:
session_id
scenarios
(contacts_show,) 116
(tips_show, contacts_show) 98
(map, tips_show, contacts_show) 58
(photos_show, contacts_show) 54
(search, contacts_show) 27
(search, photos_show, contacts_show) 20
(tips_click, tips_show, contacts_show) 15
(search, tips_show, contacts_show) 13
(contacts_call, contacts_show) 11
(map, advert_open, tips_show, contacts_show) 7
(map, contacts_show) 6
(photos_show, search, contacts_show) 6
(tips_show, map, contacts_show) 5
(advert_open, contacts_show) 4
(contacts_call, photos_show, contacts_show) 4
(search, map, tips_show, contacts_show) 4
(search, favorites_add, photos_show, contacts_show) 3
(search, map, advert_open, tips_show, contacts_show) 3
(search, contacts_call, contacts_show) 3
(map, advert_open, contacts_show) 3

Уберем события tips_show из датасета и посмотрим сценарии

In [46]:
sessions_2 = sessions.query('event_name != "tips_show"')
sessions_2
Out[46]:
session_start event_name user_id dt
0 2019-10-07 00:00:00.431357 advert_open 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07
4 2019-10-07 00:00:56.319813 advert_open cf7eda61-9349-469f-ac27-e5b6f5ec475c 2019-10-07
6 2019-10-07 00:01:27.770232 advert_open 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07
8 2019-10-07 00:01:49.732803 advert_open cf7eda61-9349-469f-ac27-e5b6f5ec475c 2019-10-07
9 2019-10-07 00:01:54.958298 advert_open 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07
... ... ... ... ...
74185 2019-11-03 23:49:19.075954 tips_click 28fccdf4-7b9e-42f5-bc73-439a265f20e9 2019-11-03
74187 2019-11-03 23:50:05.753036 tips_click c10055f0-0b47-477a-869e-d391b31fdf8f 2019-11-03
74188 2019-11-03 23:51:08.879296 tips_click c10055f0-0b47-477a-869e-d391b31fdf8f 2019-11-03
74189 2019-11-03 23:52:01.835490 tips_click c10055f0-0b47-477a-869e-d391b31fdf8f 2019-11-03
74194 2019-11-03 23:56:57.041825 search 20850c8f-4135-4059-b13b-198d3ac59902 2019-11-03

34142 rows × 4 columns

In [47]:
# создаем профиль пользователей с последним действием
profiles_last_2 = (
        sessions_2.sort_values(by=['user_id', 'session_start'])
        .groupby(['user_id', 'dt'])
        .agg({'event_name': 'last'})
        .rename(columns={'event_name': 'last_event', 'dt': 'last_dt'})
        .reset_index()
    )

profiles_last_2
Out[47]:
user_id dt last_event
0 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-09 map
1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-21 map
2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-22 map
3 00157779-810c-4498-9e05-a1e9e3cedf93 2019-10-19 photos_show
4 00157779-810c-4498-9e05-a1e9e3cedf93 2019-10-20 photos_show
... ... ... ...
6127 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-10-28 contacts_show
6128 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-10-29 contacts_show
6129 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-10-30 contacts_show
6130 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-02 contacts_show
6131 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 contacts_show

6132 rows × 3 columns

In [48]:
# соединим таблицы, чтобы получить к каждому last_event - событие с event_name
result_raw_last_2 = profiles_last_2.merge(
    sessions_2[['user_id', 'session_start', 'event_name', 'dt']], on=['user_id', 'dt'], how='left'
).reset_index().sort_values(['user_id', 'session_start'])

result_raw_last_2['session_dt'] = result_raw_last_2['session_start'].dt.date

result_raw_last_2
Out[48]:
index user_id dt last_event session_start event_name session_dt
0 0 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-09 map 2019-10-09 18:33:55.577963 map 2019-10-09
1 1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-09 map 2019-10-09 18:35:28.260975 map 2019-10-09
2 2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-21 map 2019-10-21 19:53:38.767230 map 2019-10-21
3 3 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-21 map 2019-10-21 19:56:49.417415 map 2019-10-21
4 4 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-22 map 2019-10-22 11:18:14.635436 map 2019-10-22
... ... ... ... ... ... ... ...
34137 34137 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 contacts_show 2019-11-03 14:38:51.134084 contacts_show 2019-11-03
34138 34138 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 contacts_show 2019-11-03 14:41:24.780546 contacts_show 2019-11-03
34139 34139 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 contacts_show 2019-11-03 14:42:26.444553 contacts_show 2019-11-03
34140 34140 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 contacts_show 2019-11-03 15:48:05.420247 contacts_show 2019-11-03
34141 34141 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 contacts_show 2019-11-03 15:51:57.899997 contacts_show 2019-11-03

34142 rows × 7 columns

In [49]:
# группируем датасет по session_dt, user_id, last_event
# тем самым получаем количество действий пользователя в день
# а индексы будут означать session_id каждой сессии, их переименуем в session_id

final_df_last_2 = result_raw_last_2.pivot_table(
index = ['session_dt', 'user_id', 'last_event'],
values = 'index',
aggfunc = 'count'
).reset_index().rename(columns = {'index': 'cnt_sessions'})

final_df_last_2 = final_df_last_2.reset_index().rename(columns = {'index': 'session_id'})
final_df_last_2
Out[49]:
session_id session_dt user_id last_event cnt_sessions
0 0 2019-10-07 00e714c6-66b9-4091-bce0-8589bd65b77c search 2
1 1 2019-10-07 020292ab-89bc-4156-9acf-68bc2783f894 advert_open 13
2 2 2019-10-07 032065eb-5bd5-4c5c-a4df-c60aa44c1e29 search 3
3 3 2019-10-07 057a4dc6-0698-4662-ba56-64f2bc6e0adc contacts_show 3
4 4 2019-10-07 063c0b06-6d9d-4580-8d03-f1c8f19ebfa1 photos_show 5
... ... ... ... ... ...
6127 6127 2019-11-03 f6f94ebe-e69a-4ae3-9fb0-312d52d35826 photos_show 3
6128 6128 2019-11-03 f73fb5e2-7b99-45a0-bffa-29ff320399c1 contacts_show 5
6129 6129 2019-11-03 f8cfaf96-f241-4da7-ae8a-618aaf3a7fee advert_open 1
6130 6130 2019-11-03 f9c19253-73e7-4b9e-9630-45353a792248 contacts_call 3
6131 6131 2019-11-03 fffb9e79-b927-4dbb-9b48-7fd09b23a62b contacts_show 7

6132 rows × 5 columns

In [50]:
# далее присоединяем к прошлой таблице наш датасет, чтобы дальше можно было сгруппировать по event_name
# избавимся от повторяющихся событий в рамках сессии и оставляем последние действия

final_df_last_2 = final_df_last_2.merge(
    result_raw_last_2, on = ['user_id', 'session_dt', 'last_event'], how = 'left'
).drop_duplicates(subset = ['session_id', 'event_name'], keep='last').reset_index(drop=True)

final_df_last_2
Out[50]:
session_id session_dt user_id last_event cnt_sessions index dt session_start event_name
0 0 2019-10-07 00e714c6-66b9-4091-bce0-8589bd65b77c search 2 200 2019-10-07 2019-10-07 22:01:43.074554 search
1 1 2019-10-07 020292ab-89bc-4156-9acf-68bc2783f894 advert_open 13 275 2019-10-07 2019-10-07 00:15:47.030323 map
2 1 2019-10-07 020292ab-89bc-4156-9acf-68bc2783f894 advert_open 13 277 2019-10-07 2019-10-07 00:18:37.324815 advert_open
3 2 2019-10-07 032065eb-5bd5-4c5c-a4df-c60aa44c1e29 search 3 316 2019-10-07 2019-10-07 13:19:35.882141 search
4 3 2019-10-07 057a4dc6-0698-4662-ba56-64f2bc6e0adc contacts_show 3 650 2019-10-07 2019-10-07 10:43:49.168937 search
... ... ... ... ... ... ... ... ... ...
10127 6129 2019-11-03 f8cfaf96-f241-4da7-ae8a-618aaf3a7fee advert_open 1 33297 2019-11-03 2019-11-03 15:01:08.122940 advert_open
10128 6130 2019-11-03 f9c19253-73e7-4b9e-9630-45353a792248 contacts_call 3 33337 2019-11-03 2019-11-03 11:38:41.424157 search
10129 6130 2019-11-03 f9c19253-73e7-4b9e-9630-45353a792248 contacts_call 3 33338 2019-11-03 2019-11-03 11:39:32.218473 contacts_show
10130 6130 2019-11-03 f9c19253-73e7-4b9e-9630-45353a792248 contacts_call 3 33339 2019-11-03 2019-11-03 11:39:43.578085 contacts_call
10131 6131 2019-11-03 fffb9e79-b927-4dbb-9b48-7fd09b23a62b contacts_show 7 34141 2019-11-03 2019-11-03 15:51:57.899997 contacts_show

10132 rows × 9 columns

In [51]:
# выведем все сценарии для каждой сессии:
# сгруппируем по session_id и добавим вывод сценариев с функцией лямбда и добавлением действий поочердно в кортеж

scenarios_2 = (final_df_last_2
             .groupby('session_id')
             .agg({'event_name':lambda x: tuple(x)})
             .reset_index()
             .rename(columns = {'event_name': 'scenarios'})
            )
scenarios_2
Out[51]:
session_id scenarios
0 0 (search,)
1 1 (map, advert_open)
2 2 (search,)
3 3 (search, contacts_show)
4 4 (photos_show,)
... ... ...
6127 6127 (photos_show,)
6128 6128 (search, contacts_show)
6129 6129 (advert_open,)
6130 6130 (search, contacts_show, contacts_call)
6131 6131 (contacts_show,)

6132 rows × 2 columns

In [52]:
# посчитаем количество уникальных сценарий

scenarios_2['scenarios'].nunique()
Out[52]:
282

282 уникальных сценария без событий tips_show.

Для решения основных вопросов исследования - нам нужны только те, в которых последнее действие - целевое.

In [53]:
# добавим к нашей таблице колонку scenarios
final_df_last_2 = final_df_last_2.merge(scenarios_2, on='session_id', how='left')
final_df_last_2

final_df_last_2
Out[53]:
session_id session_dt user_id last_event cnt_sessions index dt session_start event_name scenarios
0 0 2019-10-07 00e714c6-66b9-4091-bce0-8589bd65b77c search 2 200 2019-10-07 2019-10-07 22:01:43.074554 search (search,)
1 1 2019-10-07 020292ab-89bc-4156-9acf-68bc2783f894 advert_open 13 275 2019-10-07 2019-10-07 00:15:47.030323 map (map, advert_open)
2 1 2019-10-07 020292ab-89bc-4156-9acf-68bc2783f894 advert_open 13 277 2019-10-07 2019-10-07 00:18:37.324815 advert_open (map, advert_open)
3 2 2019-10-07 032065eb-5bd5-4c5c-a4df-c60aa44c1e29 search 3 316 2019-10-07 2019-10-07 13:19:35.882141 search (search,)
4 3 2019-10-07 057a4dc6-0698-4662-ba56-64f2bc6e0adc contacts_show 3 650 2019-10-07 2019-10-07 10:43:49.168937 search (search, contacts_show)
... ... ... ... ... ... ... ... ... ... ...
10127 6129 2019-11-03 f8cfaf96-f241-4da7-ae8a-618aaf3a7fee advert_open 1 33297 2019-11-03 2019-11-03 15:01:08.122940 advert_open (advert_open,)
10128 6130 2019-11-03 f9c19253-73e7-4b9e-9630-45353a792248 contacts_call 3 33337 2019-11-03 2019-11-03 11:38:41.424157 search (search, contacts_show, contacts_call)
10129 6130 2019-11-03 f9c19253-73e7-4b9e-9630-45353a792248 contacts_call 3 33338 2019-11-03 2019-11-03 11:39:32.218473 contacts_show (search, contacts_show, contacts_call)
10130 6130 2019-11-03 f9c19253-73e7-4b9e-9630-45353a792248 contacts_call 3 33339 2019-11-03 2019-11-03 11:39:43.578085 contacts_call (search, contacts_show, contacts_call)
10131 6131 2019-11-03 fffb9e79-b927-4dbb-9b48-7fd09b23a62b contacts_show 7 34141 2019-11-03 2019-11-03 15:51:57.899997 contacts_show (contacts_show,)

10132 rows × 10 columns

In [54]:
# оставим в датасете только те данные, где последнее действие - contacts_show
contacts_show_2 = final_df_last_2.query('last_event == "contacts_show"')
contacts_show_2
Out[54]:
session_id session_dt user_id last_event cnt_sessions index dt session_start event_name scenarios
4 3 2019-10-07 057a4dc6-0698-4662-ba56-64f2bc6e0adc contacts_show 3 650 2019-10-07 2019-10-07 10:43:49.168937 search (search, contacts_show)
5 3 2019-10-07 057a4dc6-0698-4662-ba56-64f2bc6e0adc contacts_show 3 651 2019-10-07 2019-10-07 11:28:52.489988 contacts_show (search, contacts_show)
24 16 2019-10-07 136b7b37-2bd4-4718-b14a-e38bc3d6d112 contacts_show 1 3251 2019-10-07 2019-10-07 21:38:02.944592 contacts_show (contacts_show,)
36 24 2019-10-07 1a3361d1-2002-4389-a669-ecb06ea7a90a contacts_show 1 4257 2019-10-07 2019-10-07 10:19:49.080484 contacts_show (contacts_show,)
47 29 2019-10-07 1fc68b5e-374b-4fc8-83cf-ac2307a8556c contacts_show 1 5027 2019-10-07 2019-10-07 20:32:14.100803 contacts_show (contacts_show,)
... ... ... ... ... ... ... ... ... ... ...
10121 6125 2019-11-03 ec70be94-3ea7-4ac2-90fb-da1a044d7e30 contacts_show 14 31848 2019-11-03 2019-11-03 20:12:29.063329 search (search, contacts_show)
10122 6125 2019-11-03 ec70be94-3ea7-4ac2-90fb-da1a044d7e30 contacts_show 14 31860 2019-11-03 2019-11-03 23:37:32.236349 contacts_show (search, contacts_show)
10125 6128 2019-11-03 f73fb5e2-7b99-45a0-bffa-29ff320399c1 contacts_show 5 33124 2019-11-03 2019-11-03 15:58:59.729391 search (search, contacts_show)
10126 6128 2019-11-03 f73fb5e2-7b99-45a0-bffa-29ff320399c1 contacts_show 5 33125 2019-11-03 2019-11-03 16:07:10.499238 contacts_show (search, contacts_show)
10131 6131 2019-11-03 fffb9e79-b927-4dbb-9b48-7fd09b23a62b contacts_show 7 34141 2019-11-03 2019-11-03 15:51:57.899997 contacts_show (contacts_show,)

1502 rows × 10 columns

In [55]:
(contacts_show_2
 .groupby('scenarios')
 .agg({'session_id': 'nunique'})
 .sort_values(by='session_id', ascending=False)
).head(20)
Out[55]:
session_id
scenarios
(contacts_show,) 402
(map, contacts_show) 137
(search, contacts_show) 73
(photos_show, contacts_show) 55
(tips_click, contacts_show) 31
(search, photos_show, contacts_show) 20
(map, advert_open, contacts_show) 13
(contacts_call, contacts_show) 11
(advert_open, contacts_show) 10
(search, map, contacts_show) 10
(favorites_add, contacts_show) 9
(map, tips_click, contacts_show) 7
(map, search, advert_open, contacts_show) 7
(photos_show, search, contacts_show) 6
(search, map, advert_open, contacts_show) 6
(tips_click, map, contacts_show) 5
(map, favorites_add, contacts_show) 4
(map, search, contacts_show) 4
(contacts_call, photos_show, contacts_show) 4
(tips_click, search, contacts_show) 3

Для дальнейшего анализа возьмём 4 самые популярные сценарии:

  • (map, contacts_show)
  • (search, contacts_show)
  • (photos_show, contacts_show)
  • (search, photos_show, contacts_show)

Сценарий (contacts_show,) рассматривать не будем, т.к. данный сценарий скорее является исключением, чем расскажет нам о поведении пользователей. Скорее всего данный сценарий возникает, когда пользователь проходит по ссылке на объявление и поэтому происходит сразу целевое действие.

Сценарий (tips_click, contacts_show) возникает только после события tips_show, который мы не рассматриваем для данного задания. Поэтому данный сценарий тоже не будем рассматривать, а лучше возмём следующий самый популярный сценарий с 3 событиями в сценарии - (search, photos_show, contacts_show).

Построить воронки по основным сценариям в разрезе уникальных пользователей.¶
In [56]:
contacts_show_2
Out[56]:
session_id session_dt user_id last_event cnt_sessions index dt session_start event_name scenarios
4 3 2019-10-07 057a4dc6-0698-4662-ba56-64f2bc6e0adc contacts_show 3 650 2019-10-07 2019-10-07 10:43:49.168937 search (search, contacts_show)
5 3 2019-10-07 057a4dc6-0698-4662-ba56-64f2bc6e0adc contacts_show 3 651 2019-10-07 2019-10-07 11:28:52.489988 contacts_show (search, contacts_show)
24 16 2019-10-07 136b7b37-2bd4-4718-b14a-e38bc3d6d112 contacts_show 1 3251 2019-10-07 2019-10-07 21:38:02.944592 contacts_show (contacts_show,)
36 24 2019-10-07 1a3361d1-2002-4389-a669-ecb06ea7a90a contacts_show 1 4257 2019-10-07 2019-10-07 10:19:49.080484 contacts_show (contacts_show,)
47 29 2019-10-07 1fc68b5e-374b-4fc8-83cf-ac2307a8556c contacts_show 1 5027 2019-10-07 2019-10-07 20:32:14.100803 contacts_show (contacts_show,)
... ... ... ... ... ... ... ... ... ... ...
10121 6125 2019-11-03 ec70be94-3ea7-4ac2-90fb-da1a044d7e30 contacts_show 14 31848 2019-11-03 2019-11-03 20:12:29.063329 search (search, contacts_show)
10122 6125 2019-11-03 ec70be94-3ea7-4ac2-90fb-da1a044d7e30 contacts_show 14 31860 2019-11-03 2019-11-03 23:37:32.236349 contacts_show (search, contacts_show)
10125 6128 2019-11-03 f73fb5e2-7b99-45a0-bffa-29ff320399c1 contacts_show 5 33124 2019-11-03 2019-11-03 15:58:59.729391 search (search, contacts_show)
10126 6128 2019-11-03 f73fb5e2-7b99-45a0-bffa-29ff320399c1 contacts_show 5 33125 2019-11-03 2019-11-03 16:07:10.499238 contacts_show (search, contacts_show)
10131 6131 2019-11-03 fffb9e79-b927-4dbb-9b48-7fd09b23a62b contacts_show 7 34141 2019-11-03 2019-11-03 15:51:57.899997 contacts_show (contacts_show,)

1502 rows × 10 columns

Воронка для сценария (map, contacts_show)¶
In [57]:
# оставим в датасете только действия map | (map)
map = final_df_last_2.query('event_name == "map"')

# посчитаем их количество
print('Количество пользователей совершивших (search):', map['user_id'].nunique())

# добавим пользователей, которые совершили данное действие в лист
map_users_list = map['user_id'].unique().tolist()

# оставим в датасете все действия пользователей, которые совершили действие map
map_funnel = final_df_last_2.query('user_id in @map_users_list')

# теперь в прошлой таблице оставим только тех, кто совершил (map, contacts_show)  | (map, contacts_show)
contacts_show = map_funnel.query('event_name == "contacts_show"')

# посчитаем их количество
print('Количество пользователей совершивших (search, contacts_show):', contacts_show['user_id'].nunique())
Количество пользователей совершивших (search): 1456
Количество пользователей совершивших (search, contacts_show): 289
In [58]:
# построим воронку для сценария (tips_show, map, contacts_show)
fig = go.Figure(go.Funnel(
    y = ['map', 'contacts_show'],
    x = [map['user_id'].nunique(), contacts_show['user_id'].nunique()],
    textposition = 'inside',
    textinfo = 'value+percent initial+percent previous'
))
fig.update_layout(title='Воронка для сценария search-contacts_show')
fig.show()
In [59]:
# создадим таблицу с RR и CR
funnel = pd.DataFrame({'event_name': ['map',
                                      'contacts_show'],
                       'cnt_users': [map['user_id'].nunique(),
                                     contacts_show['user_id'].nunique()]})

funnel = funnel.sort_values(by='cnt_users', ascending=False)

# посчитаем долю пользователей, которые совершили действия по данному событию
funnel['ratio, %'] = funnel['cnt_users'] / final_df_last_2['user_id'].nunique() * 100

# посчитаем конверсию пользователей от события к событию
funnel['conversion, %'] = (funnel['cnt_users'] / funnel['cnt_users'].shift(1))

# посчитаем долю пользователей, которые доходят до ЦС
funnel['funnel, %'] = (funnel['cnt_users'] / funnel['cnt_users'].values[0])

funnel
Out[59]:
event_name cnt_users ratio, % conversion, % funnel, %
0 map 1456 40.60 NaN 1.00
1 contacts_show 289 8.06 0.20 0.20

CR от открытия карты объявления к целевому действию - 20%.

RR для действия map от общего количества уникальных пользователей составляет 40,6%.\ RR для действия contacts_show от общего количества пользователей составляет 8,06%.

Воронка для сценария (search, contacts_show)¶
In [60]:
# оставим в датасете только действия search | (search)
search = final_df_last_2.query('event_name == "search"')

# посчитаем их количество
print('Количество пользователей совершивших (search):', search['user_id'].nunique())

# добавим пользователей, которые совершили данное действие в лист
search_users_list = search['user_id'].unique().tolist()

# оставим в датасете все действия пользователей, которые совершили действие search
search_funnel = final_df_last_2.query('user_id in @search_users_list')

# теперь в прошлой таблице оставим только тех, кто совершил (search, contacts_show)  | (search, contacts_show)
contacts_show = search_funnel.query('event_name == "contacts_show"')

# посчитаем их количество
print('Количество пользователей совершивших (search, contacts_show):', contacts_show['user_id'].nunique())
Количество пользователей совершивших (search): 1666
Количество пользователей совершивших (search, contacts_show): 377
In [61]:
# построим воронку для сценария (tips_show, map, contacts_show)
fig = go.Figure(go.Funnel(
    y = ['search', 'contacts_show'],
    x = [search['user_id'].nunique(), contacts_show['user_id'].nunique()],
    textposition = 'inside',
    textinfo = 'value+percent initial+percent previous'
))
fig.update_layout(title='Воронка для сценария search-contacts_show')
fig.show()
In [62]:
# создадим таблицу с RR и CR
funnel = pd.DataFrame({'event_name': ['search',
                                      'contacts_show'],
                       'cnt_users': [search['user_id'].nunique(),
                                     contacts_show['user_id'].nunique()]})

funnel = funnel.sort_values(by='cnt_users', ascending=False)

# посчитаем долю пользователей, которые совершили действия по данному событию
funnel['ratio, %'] = funnel['cnt_users'] / final_df_last_2['user_id'].nunique() * 100

# посчитаем конверсию пользователей от события к событию
funnel['conversion, %'] = (funnel['cnt_users'] / funnel['cnt_users'].shift(1))

# посчитаем долю пользователей, которые доходят до ЦС
funnel['funnel, %'] = (funnel['cnt_users'] / funnel['cnt_users'].values[0])

funnel
Out[62]:
event_name cnt_users ratio, % conversion, % funnel, %
0 search 1666 46.46 NaN 1.00
1 contacts_show 377 10.51 0.23 0.23

CR от поиска объявления к целевому действию - 23%.

RR для действия searсh от общего количества уникальных пользователей составляет 38,81% - это значит, что 38,81% от всех пользователей занимались ручным поиском объявлений.\ RR для действия contacts_show от общего количества пользователей составляет 8,78%.

Воронка для сценария (photos_show, contacts_show)¶
In [63]:
# оставим в датасете только действия photos_show | (photos_show)
photos_show = final_df_last_2.query('event_name == "photos_show"')

# посчитаем их количество
print('Количество пользователей совершивших (photos_show):', photos_show['user_id'].nunique())

# добавим пользователей, которые совершили данное действие в лист
photos_show_users_list = photos_show['user_id'].unique().tolist()

# оставим в датасете все действия пользователей, которые совершили действие photos_show
photos_show_funnel = final_df_last_2.query('user_id in @photos_show_users_list')

# теперь в прошлой таблице оставим только тех, кто совершил contacts_show | (photos_show, contacts_show)
contacts_show = photos_show_funnel.query('event_name == "contacts_show"')

# посчитаем их количество
print('Количество пользователей совершивших (photos_show, contacts_show):', contacts_show['user_id'].nunique())
Количество пользователей совершивших (photos_show): 1095
Количество пользователей совершивших (photos_show, contacts_show): 339
In [64]:
# построим воронку для сценария (tips_show, contacts_show)
fig = go.Figure(go.Funnel(
    y = ['photos_show', 'contacts_show'],
    x = [photos_show['user_id'].nunique(), contacts_show['user_id'].nunique()],
    textposition = 'inside',
    textinfo = 'value+percent initial+percent previous'
))
fig.update_layout(title='Воронка для сценария photos_show-contacts_show')
fig.show()
In [65]:
# создадим таблицу с RR и CR
funnel = pd.DataFrame({'event_name': ['photos_show',
                                      'contacts_show'],
                       'cnt_users': [photos_show['user_id'].nunique(),
                                     contacts_show['user_id'].nunique()]})

funnel = funnel.sort_values(by='cnt_users', ascending=False)

# посчитаем долю пользователей, которые совершили действия по данному событию
funnel['ratio, %'] = funnel['cnt_users'] / final_df_last_2['user_id'].nunique() * 100

# посчитаем конверсию пользователей от события к событию
funnel['conversion, %'] = (funnel['cnt_users'] / funnel['cnt_users'].shift(1))

# посчитаем долю пользователей, которые доходят до ЦС
funnel['funnel, %'] = (funnel['cnt_users'] / funnel['cnt_users'].values[0])

funnel
Out[65]:
event_name cnt_users ratio, % conversion, % funnel, %
0 photos_show 1095 30.54 NaN 1.00
1 contacts_show 339 9.45 0.31 0.31

CR от просмотревших фото объявления к целевому действию - 31%.

RR для действия photos_show от общего количества уникальных пользователей составляет 25,51% - это значит, что 25,5% от всех пользователей просмотрели фото в объявлении.\ RR для действия contacts_show от общего количества пользователей составляет 7,9%.

Воронка для сценария (search, photos_show, contacts_show)¶
In [66]:
# т.к. мы уже провели расчеты по совершавшим search - сразу посчитаем их количество
print('Количество пользователей совершивших (search):', search['user_id'].nunique())

# оставим только тех, кто совершил photos_show, после search | (search, photos_show) 
photos_show = search_funnel.query('event_name == "photos_show"')

# посчитаем их количество
print('Количество пользователей совершивших (search, photos_show):', photos_show['user_id'].nunique())

# добавим пользователей, которые совершили действие (search, photos_show) в лист
photos_show_users_list = photos_show['user_id'].unique().tolist()

# выведем все действия тех пользователей, которые совершили действие (search, photos_show)
photos_show_funnel = final_df_last_2.query(('user_id in @photos_show_users_list'))

# оставим только тех, кто совершил (search, photos_show, contacts_show)  | (search, photos_show, contacts_show)
contacts_show = photos_show_funnel.query('event_name == "contacts_show"')

# посчитаем их количество
print('Количество пользователей совершивших (search, photos_show, contacts_show):', contacts_show['user_id'].nunique())
Количество пользователей совершивших (search): 1666
Количество пользователей совершивших (search, photos_show): 647
Количество пользователей совершивших (search, photos_show, contacts_show): 192
In [67]:
# построим воронку для сценария (search, photos_show, contacts_show)
fig = go.Figure(go.Funnel(
    y = ['search', 'photos_show', 'contacts_show'],
    x = [search['user_id'].nunique(), photos_show['user_id'].nunique(), contacts_show['user_id'].nunique()],
    textposition = 'auto',
    textinfo = 'value+percent initial+percent previous'
))
fig.update_layout(title='Воронка для сценария (search, photos_show, contacts_show)')
fig.show()
In [68]:
# создадим таблицу с RR и CR
funnel = pd.DataFrame({'event_name': ['search',
                                      'photos_show',
                                      'contacts_show'],
                       'cnt_users': [search['user_id'].nunique(),
                                     photos_show['user_id'].nunique(),
                                     contacts_show['user_id'].nunique()]})

funnel = funnel.sort_values(by='cnt_users', ascending=False)

# посчитаем долю пользователей, которые совершили действия по данному событию
funnel['ratio, %'] = funnel['cnt_users'] / final_df_last_2['user_id'].nunique() * 100

# посчитаем конверсию пользователей от события к событию
funnel['conversion, %'] = (funnel['cnt_users'] / funnel['cnt_users'].shift(1))

# посчитаем долю пользователей, которые доходят до ЦС
funnel['funnel, %'] = (funnel['cnt_users'] / funnel['cnt_users'].values[0])

funnel
Out[68]:
event_name cnt_users ratio, % conversion, % funnel, %
0 search 1666 46.46 NaN 1.00
1 photos_show 647 18.04 0.39 0.39
2 contacts_show 192 5.35 0.30 0.12

CR от search к photos_show составляет 66%, от photos_show к целевому действию - 31%. Очень достойные показатели, это значит, что если пользователь посмотрел фото объявление после поиска, то он с вероятностью 31% добавит контакты продавца. Нужно каким-то образом подстегивать людей на данные действия. Может поможет геймификация?

RR для действия search от общего количества уникальных пользователей составляет 38,81%. RR для действия photos_show от общего количества пользователей составляет 25,51%. RR для действия contacts_show от общего количества пользователей составляет 7,9%.

Один из верных способов, по моему мнению, это сделать геймификацию: Платить условные баллы (которые можно потратить на рекламу своего объявления и т.п.) за определенные действия, которые интересены для бизнеса (такие как: произвести 10 поисков в день, добавить 5 товаров в избранное или заходить неделю подряд).

Промежуточный вывод¶

СЦЕНАРИЙ (tips_show, contacts_show)

CR от просмотревших рекомендованное объявление к целевому действию - 18,4%.

RR для действия tips_show от общего количества уникальных пользователей составляет 65,25% - это значит, что 65% от всех пользователей увидели рекомендованное объявление, данное действие является контролируемой и не зависит от поведения пользователей. RR для действия contacts_show от общего количества пользователей составляет 12,02%.

СЦЕНАРИЙ (tips_show, map, contacts_show)

CR от tips_show к map составляет 48,3%, от map к целевому действию - 20,3%.

RR для действия tips_show от общего количества уникальных пользователей составляет 65,25% - это значит, что 65% от всех пользователей увидели рекомендованное объявление, данное действие является контролируемой и не зависит от поведения пользователей. RR для действия map от общего количества пользователей составляет 31,49%. RR для действия contacts_show от общего количества пользователей составляет 6,41%.

Лишь 10% от tips_show дошли до целевого действия.

СЦЕНАРИЙ (tips_show, tips_click, contacts_show)

CR от tips_show к tips_click составляет 10,6%, от tips_click к целевому действию - 30,6%. Очень достойный показатель, это значит, что рекомендации очень хорошо настроены и пользователь, который прошёл по рекомендации, с вероятностью 30% добавит контакты продавца. Раз рекомендации работают хорошо, значит нужно, чтобы люди чаще переходили по ним.

RR для действия tips_show от общего количества уникальных пользователей составляет 65,25% - это значит, что 65% от всех пользователей увидели рекомендованное объявление, данное действие является контролируемой и не зависит от поведения пользователей. RR для действия tips_click от общего количества пользователей составляет 6,92%. RR для действия contacts_show от общего количества пользователей составляет 2,12%.

Проблема в том, что люди не доверяют рекомендациям, либо они привыкли не обращать на них свое внимание. И именно из-за этой проблемы лишь 2,9% от tips_show доходят до целевого действия.

Самый верный вариант, по моему мнению, это сделать геймификацию: Платить условные баллы (которые можно потратить на рекламу своего объявления и т.п.) за определенное количество tips_click. Данный способ может сработать.

СЦЕНАРИЙ (map, contacts_show)

CR от открытия карты объявления к целевому действию - 20%.

RR для действия map от общего количества уникальных пользователей составляет 40,6%.\ RR для действия contacts_show от общего количества пользователей составляет 8,06%.

СЦЕНАРИЙ (search, contacts_show)

CR от поиска объявления к целевому действию - 23%.

RR для действия searxh от общего количества уникальных пользователей составляет 38,81% - это значит, что 38,81% от всех пользователей занимались ручным поиском объявлений.\ RR для действия contacts_show от общего количества пользователей составляет 8,78%.

СЦЕНАРИЙ (photos_show, contacts_show)

CR от просмотревших фото объявления к целевому действию - 31%.

RR для действия photos_show от общего количества уникальных пользователей составляет 25,51% - это значит, что 25,5% от всех пользователей просмотрели фото в объявлении.\ RR для действия contacts_show от общего количества пользователей составляет 7,9%.

СЦЕНАРИЙ (search, photos_show, contacts_show)

CR от search к photos_show составляет 66%, от photos_show к целевому действию - 31%. Очень достойные показатели, это значит, что если пользователь посмотрел фото объявление после поиска, то он с вероятностью 31% добавит контакты продавца. Нужно каким-то образом подстегивать людей на данные действия. Может поможет геймификация?

RR для действия search от общего количества уникальных пользователей составляет 38,81%. RR для действия photos_show от общего количества пользователей составляет 25,51%. RR для действия contacts_show от общего количества пользователей составляет 7,9%.

ВЫВОД

Для увеличения активности пользователей - можно сделать сделать геймификацию:\ Платить условные баллы (которые можно потратить на рекламу своего объявления и т.п.) за определенные действия, которые интересены для бизнеса (такие как: произвести 10 поисков в день, добавить 5 товаров в избранное или заходить неделю подряд). Данный способ поможет поднять активность пользователей.

Также для увеличения активности пользователей можно расширить функционал приложения добавляя новые возможности: возможность выбора транспортной компанией в случае успешной продажи; возможность добавления (также их привлечение) компаний по продажам вещей, например, секондхенды или оффлайн магазины и т.д.

Для улучшения работы приложения не в количестве аудитории, а в качестве стоит подумать над новой рекомендательной системой или её улучшением. Довольно большое количество событий tips_show было показано пользователям, а конверсия от tips_show к tips_click составляет всего лишь 10,6%. Возможно пользователи не видят этих рекомендаций, может стоит увеличить их в размере или поставить в более видное место, может они просто не хотят переходить по ним (в последнем случае поможет геймификация). Стоит разобраться с этой проблемой.\ Сама рекомендательная система показывает 30% конверсию от tips_click к целевому действию, над этими цифрами тоже можно поработать улучшив саму рекомендательную систему.

Оцените, какие действия чаще совершают те пользователи, которые просматривают контакты.¶
  • Рассчитать относительную частоту событий в разрезе двух групп пользователей:
  1. группа пользователей, которые смотрели контакты contacts_show
  2. группа пользователей, которые не смотрели контакты contacts_show

Для ответа на вопрос - поделим датасет на:

  1. Группу пользователей (user_id), которые смотрели контакты (event_name == contacts_show)
  2. Группу пользователей (user_id), которые не смотрели контакты (event_name != contacts_show)
In [69]:
df
Out[69]:
session_start event_name user_id dt source
0 2019-10-07 00:00:00.431357 advert_open 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
1 2019-10-07 00:00:01.236320 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
2 2019-10-07 00:00:07.039334 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
3 2019-10-07 00:01:27.770232 advert_open 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
4 2019-10-07 00:01:34.804591 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
... ... ... ... ... ...
74192 2019-11-03 23:46:47.068179 map d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google
74193 2019-11-03 23:46:58.914787 advert_open d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google
74194 2019-11-03 23:47:01.232230 tips_show d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google
74195 2019-11-03 23:47:47.475102 advert_open d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google
74196 2019-11-03 23:47:50.087645 tips_show d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google

74197 rows × 5 columns

In [70]:
# разделим датасет на 2 части: на пользователей, которые смотрели и не смотрели контакты
all_users = df
print('Количество всех пользователей:', all_users['user_id'].nunique())

show_users = df.query('event_name == "contacts_show"')
print('Количество пользователей, которые смотрели контакты:', show_users['user_id'].nunique())
show_users_list = show_users['user_id'].unique().tolist()
show_users = df.query('user_id in @show_users_list')

dont_show_users = df.query('user_id not in @show_users_list')
print('Количество пользователей, которые не смотрели контакты:', dont_show_users['user_id'].nunique())
Количество всех пользователей: 4293
Количество пользователей, которые смотрели контакты: 981
Количество пользователей, которые не смотрели контакты: 3312
In [71]:
show_users
Out[71]:
session_start event_name user_id dt source
121 2019-10-07 00:02:07.374346 tips_show 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 2019-10-07 yandex
122 2019-10-07 00:05:16.003328 contacts_show 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 2019-10-07 yandex
123 2019-10-07 00:06:01.349291 tips_show 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 2019-10-07 yandex
124 2019-10-07 00:06:56.367054 tips_show 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 2019-10-07 yandex
125 2019-10-07 00:09:24.585200 tips_show 8c356c42-3ba9-4cb6-80b8-3f868d0192c3 2019-10-07 yandex
... ... ... ... ... ...
74187 2019-11-03 23:46:31.298524 contacts_show c10055f0-0b47-477a-869e-d391b31fdf8f 2019-11-03 yandex
74188 2019-11-03 23:48:47.344430 tips_click c10055f0-0b47-477a-869e-d391b31fdf8f 2019-11-03 yandex
74189 2019-11-03 23:50:05.753036 tips_click c10055f0-0b47-477a-869e-d391b31fdf8f 2019-11-03 yandex
74190 2019-11-03 23:51:08.879296 tips_click c10055f0-0b47-477a-869e-d391b31fdf8f 2019-11-03 yandex
74191 2019-11-03 23:52:01.835490 tips_click c10055f0-0b47-477a-869e-d391b31fdf8f 2019-11-03 yandex

27197 rows × 5 columns

Разница в совершенных действиях¶

Данные группы отличаются только тем, что в группе смотревших контакты присутствуют действия: contacts_show и contacts_call.

Их следует убрать, т.к. понятно, что если мы поделили группу, на совершивих и не совершивших целевое действие, то в первой группе будут данные действия, а во второй - их не будет. Поэтому чтобы разобраться в поведении пользователей эти действия стоит удалить из первой группы. contacts_call стоит удалить, т.к. данное действие является продолжением действия contacts_show.

In [72]:
show_users = show_users.query('event_name != "contacts_show" and event_name != "contacts_call"')
In [73]:
# посмотрим названия действий и количество таких событий среди смотревших контакты
show_users_events = show_users['event_name'].value_counts()
In [74]:
show_users_events.reset_index()
Out[74]:
event_name count
0 tips_show 12768
1 photos_show 3828
2 search 2084
3 advert_open 1589
4 map 1101
5 favorites_add 424
6 tips_click 333
In [75]:
# посмотрим названия действий и количество таких событий среди не смотревших контакты
dont_show_users_events = dont_show_users['event_name'].value_counts()
In [76]:
dont_show_users_events.reset_index()
Out[76]:
event_name count
0 tips_show 27287
1 photos_show 6184
2 search 4700
3 advert_open 4575
4 map 2780
5 favorites_add 993
6 tips_click 481
In [77]:
# построим круговые диаграммы по смотревшим контакты и не смотревших контакты

fig = make_subplots(rows=1, cols=2,
                   specs=[[{'type': 'domain'}, {'type': 'domain'}]],
                   subplot_titles=('Круговая диаграмма по целевому действию',
                                   'Круговая диаграмма по не целевому действию'))

fig.add_trace(
    go.Pie(labels=show_users_events.index,
                             values=show_users_events),
    row=1, col=1
)

fig.add_trace(
    go.Pie(labels=dont_show_users_events.index,
                             values=dont_show_users_events),
    row=1, col=2
)    

fig.update_layout(height=500, width=1000)
fig.show()
In [78]:
show_users_events
Out[78]:
event_name
tips_show        12768
photos_show       3828
search            2084
advert_open       1589
map               1101
favorites_add      424
tips_click         333
Name: count, dtype: int64
In [79]:
show_users_events.reset_index()
Out[79]:
event_name count
0 tips_show 12768
1 photos_show 3828
2 search 2084
3 advert_open 1589
4 map 1101
5 favorites_add 424
6 tips_click 333
In [80]:
# строим столбчатую диаграмму по смотревшим контакты
fig = px.bar(show_users_events.reset_index(),
             x='event_name',
             y='count',
             text='count',
             color_discrete_sequence = px.colors.sequential.Plotly3)

# оформляем график
fig.update_layout(title='Диаграмма по целевому действию',
                  xaxis_title='Название совершеннного действия',
                  yaxis_title='Количество совершенных действий')

# выводим график
fig.show()
In [81]:
# строим столбчатую диаграмму по не смотревшим контакты
fig = px.bar(dont_show_users_events.reset_index(),
             x='event_name',
             y='count',
             text='count',
             color_discrete_sequence = px.colors.sequential.Plotly3)

# оформляем график
fig.update_layout(title='Диаграмма по не целевому действию',
                  xaxis_title='Название действия',
                  yaxis_title='Количество действий')

# выводим график
fig.show()

Очередность по количеству действий в данных группах точно такая же, т.е. топ по количеству действий в данных группах не отличается.

Можно отметить тот факт, что пользователи, которые просмотрели контакты - более активны. Больше действий в photos_show и map, чем у тех, кто не смотрел контакты. Но у достигших целевого действия - меньше открытий объявлений (advert_open).

Разница в датах¶
In [82]:
# посмотрим названия действий и количество таких событий среди смотревших контакты
show_users_dt = show_users['dt'].value_counts()
In [83]:
show_users_dt
Out[83]:
dt
2019-10-29    1128
2019-10-23    1052
2019-10-24    1051
2019-11-01    1023
2019-10-26     969
2019-10-28     950
2019-10-21     947
2019-10-25     906
2019-10-31     896
2019-10-27     868
2019-10-30     847
2019-11-03     835
2019-10-13     834
2019-10-18     829
2019-10-22     825
2019-10-15     773
2019-10-17     723
2019-10-10     705
2019-10-16     697
2019-10-14     679
2019-10-12     647
2019-11-02     645
2019-10-11     625
2019-10-19     580
2019-10-08     575
2019-10-09     529
2019-10-07     503
2019-10-20     486
Name: count, dtype: int64
In [84]:
dont_show_users_dt = dont_show_users['dt'].value_counts()
In [85]:
dont_show_users_dt
Out[85]:
dt
2019-10-14    2123
2019-10-23    2048
2019-10-28    2005
2019-10-07    1974
2019-10-26    1958
2019-10-29    1894
2019-10-27    1876
2019-11-03    1874
2019-10-16    1829
2019-10-30    1815
2019-10-08    1782
2019-10-21    1771
2019-10-18    1733
2019-10-24    1727
2019-10-31    1709
2019-10-15    1705
2019-10-13    1690
2019-10-22    1669
2019-10-19    1641
2019-10-17    1595
2019-10-25    1505
2019-10-20    1445
2019-10-10    1413
2019-10-09    1389
2019-11-01    1319
2019-10-11    1311
2019-10-12    1132
2019-11-02    1068
Name: count, dtype: int64
In [86]:
# строим столбчатую диаграмму 
fig = px.bar(show_users_dt.reset_index(),
             x='dt',
             y='count',
             text='count',
             color_discrete_sequence = px.colors.sequential.Plotly3)

# оформляем график
fig.update_layout(title='Диаграмма по целевому действию',
                  xaxis_title='Дата совершенных действий',
                  yaxis_title='Количество совершеннных действий')

# выводим график
fig.show()
In [87]:
# строим столбчатую диаграмму 
fig = px.bar(dont_show_users_dt.reset_index(),
             x='dt',
             y='count',
             text='count',
             color_discrete_sequence = px.colors.sequential.Plotly3)

# оформляем график
fig.update_layout(title='Диаграмма по не целевому действию',
                  xaxis_title='Дата совершеннных действий',
                  yaxis_title='Количество совершенных действий')

# выводим график
fig.show()

По данным этих групп есть некая закономерность.

В диаграмме группы, достигших целевого действия - видно, что после сильной просадки (19.10) начинается увеличиваться активность, пик которой достигает (29.10), а дальше начинает падать.

А в диаграмме группы, не достигших целевого действия такой закономерности не видно. Данные за все время примерно находятся на одном уровне, хотя были 2 сильные просадки (12.10 и 02.11).

Разница в источниках¶
In [88]:
show_users_source = show_users['source'].value_counts()
In [89]:
show_users_source
Out[89]:
source
yandex    10238
google     6910
other      4979
Name: count, dtype: int64
In [90]:
dont_show_users_source = dont_show_users['source'].value_counts()
In [91]:
dont_show_users_source
Out[91]:
source
yandex    21787
other     13281
google    11932
Name: count, dtype: int64
In [92]:
# построим круговые диаграммы источников по смотревшим контакты и не смотревшим контакты

fig = make_subplots(rows=1, cols=2,
                   specs=[[{'type': 'domain'}, {'type': 'domain'}]],
                   subplot_titles=('Круговая диаграмма по целевому действию',
                                   'Круговая диаграмма по не целевому действию'))

fig.add_trace(
    go.Pie(labels=show_users_source.index,
           values=show_users_source),
    row=1, col=1
)

fig.add_trace(
    go.Pie(labels=dont_show_users_source.index,
           values=dont_show_users_source),
    row=1, col=2
)    

fig.update_layout(height=600, width=885)
fig.show()

Обе группы пользуются в равной степени yandex, но в группе достигших целевого действия на втором месте стоит google(31,2%), а в не достигших целевого действия на втором месте other (28,3%).

Промежуточный вывод¶

Подытожив, можно сказать, что разница между группами совсем маленькая, даже если где-то она и есть, то всего лишь на пару процентов. Нельзя с уверенностью сказать, что группа, которая достигает целевого действия - достигает именно из-за 'X' действия.

Но важно отметить, что ровно с третьей недели - увеличивается активность группы, совершивших целевое действие.

Проверка статистических гипотез¶

Гипотеза о различии конверсии в просмотры контактов (целевое действие) между двумя группами (group_1 - tips_show, tips_click и group_2 - tips_show)¶

Для начала нужно разбить пользователей на 2 группы:

  • На тех, кто совершил (event_name == tips_show), а следующим действием было (==) tips_click;
  • На тех, кто совершил (event_name == tips_show), а следующее действие не было (!=) tips_click.
In [93]:
all_tips_show_users = df.query('event_name == "tips_show"')
print('Количество пользователей, которым показали рекомендацию:', all_tips_show_users['user_id'].nunique())
all_tips_show_users_list = all_tips_show_users['user_id'].unique().tolist()
all_tips_show_users = df.query('user_id in @all_tips_show_users_list')

tips_click_users = all_tips_show_users.query('event_name == "tips_click"')
print('Количество пользователей, которые кликнули по рекомендации:', tips_click_users['user_id'].nunique())
tips_click_users_list = tips_click_users['user_id'].unique().tolist()
tips_click_users = df.query('user_id in @tips_click_users_list')

dont_tips_click_users = all_tips_show_users.query('user_id not in @tips_click_users_list')
print('Количество пользователей, которые увидели рекомендацию, но не кликнули:',
      dont_tips_click_users['user_id'].nunique())
Количество пользователей, которым показали рекомендацию: 2801
Количество пользователей, которые кликнули по рекомендации: 297
Количество пользователей, которые увидели рекомендацию, но не кликнули: 2504

Пользователи разбиты на группы. Далее нужно посчитать конверсию каждой группы в просмотр контактов.

Для этого найдём тех, пользователей, которые:

  • После клика по рекомендации (tips_click_users) посмотрели контакты (event_name == contacts_show) - конверсия этой группы будет показывать эффективность наших рекомендаций;
  • После показа рекомендации не кликнули по нему (dont_tips_click_users), сами нашли подходящее объявление и посмотрели контакты (event_name == contacts_show).
In [94]:
click_in_contacts_show = tips_click_users.query('event_name == "contacts_show"')
print('Количество пользователей, которые кликнули по рекомендации и поcмотрели контакты:',
      click_in_contacts_show['user_id'].nunique())
print('Конверсия в просмотр контактов данной группы:',
      round((click_in_contacts_show['user_id'].nunique() / tips_click_users['user_id'].nunique()) * 100, 1), '%')

print()

dont_click_in_contacts_show = dont_tips_click_users.query('event_name == "contacts_show"')
print('Количество пользователей, которые не кликнули по рекомендации, сами нашли объявление и посмотрели контакты:',
      dont_click_in_contacts_show['user_id'].nunique())
print('Конверсия в просмотр контактов данной группы:',
      round((dont_click_in_contacts_show['user_id'].nunique() / dont_tips_click_users['user_id'].nunique()) * 100, 1), '%')
Количество пользователей, которые кликнули по рекомендации и поcмотрели контакты: 91
Конверсия в просмотр контактов данной группы: 30.6 %

Количество пользователей, которые не кликнули по рекомендации, сами нашли объявление и посмотрели контакты: 425
Конверсия в просмотр контактов данной группы: 17.0 %

Для наглядности сделаем таблицы по двум группам и их метрикам.

In [95]:
conversion_tips_click = pd.DataFrame({'event_name': ['tips_show',
                                                     'tips_click',
                                                     'contacts_show'],
                                      'cnt_users': [all_tips_show_users['user_id'].nunique(),
                                                    tips_click_users['user_id'].nunique(),
                                                    click_in_contacts_show['user_id'].nunique()]})

conversion_tips_click = conversion_tips_click.sort_values(by='cnt_users', ascending=False)

# посчитаем долю пользователей, которые совершили действия по данному событию
conversion_tips_click['ratio, %'] = (conversion_tips_click['cnt_users'] / all_tips_show_users['user_id'].nunique()) * 100

# посчитаем конверсию пользователей от события к событию
conversion_tips_click['conversion, %'] = (conversion_tips_click['cnt_users'] / conversion_tips_click['cnt_users'].shift(1)) * 100

# посчитаем долю пользователей, которые доходят до ЦС
conversion_tips_click['funnel, %'] = (conversion_tips_click['cnt_users'] / conversion_tips_click['cnt_users'].values[0]) * 100

conversion_tips_click
Out[95]:
event_name cnt_users ratio, % conversion, % funnel, %
0 tips_show 2801 100.00 NaN 100.00
1 tips_click 297 10.60 10.60 10.60
2 contacts_show 91 3.25 30.64 3.25
In [96]:
conversion_dont_tips_click = pd.DataFrame({'event_name': ['tips_show',
                                                          'dont_tips_click',
                                                          'contacts_show'],
                                           'cnt_users': [all_tips_show_users['user_id'].nunique(),
                                                         dont_tips_click_users['user_id'].nunique(),
                                                         dont_click_in_contacts_show['user_id'].nunique()]})

conversion_dont_tips_click = conversion_dont_tips_click.sort_values(by='cnt_users', ascending=False)

# посчитаем долю пользователей, которые совершили действия по данному событию
conversion_dont_tips_click['ratio, %'] = (conversion_dont_tips_click['cnt_users'] / all_tips_show_users['user_id'].nunique()) * 100

# посчитаем конверсию пользователей от события к событию
conversion_dont_tips_click['conversion, %'] = (conversion_dont_tips_click['cnt_users'] / conversion_dont_tips_click['cnt_users'].shift(1)) * 100

# посчитаем долю пользователей, которые доходят до ЦС
conversion_dont_tips_click['funnel, %'] = (conversion_dont_tips_click['cnt_users'] / conversion_dont_tips_click['cnt_users'].values[0]) * 100

conversion_dont_tips_click
Out[96]:
event_name cnt_users ratio, % conversion, % funnel, %
0 tips_show 2801 100.00 NaN 100.00
1 dont_tips_click 2504 89.40 89.40 89.40
2 contacts_show 425 15.17 16.97 15.17

Конверсии посчитаны, можно приступать к проверке гипотез.

Напишем нулевую и альтернативную гипотезы.

H0: Различий в конверсии в целевое действие между группами (group_1 - tips_show, tips_click и group_2 - tips_show) нет;

H1: Есть различия в конверсии в целевое действие между группами (group_1 - tips_show, tips_click и group_2 - tips_show).

In [97]:
# создадим функцию для проверки гипотезы о равенстве долей

# alpha = 0.05 # критический уровень статистической значимости

def z_test(alpha):
    
    #group_1 = logs[logs['group'] == group1]
    #group_2 = logs[logs['group'] == group2]
    display('group_1:', tips_click_users)
    display('group_2:', dont_tips_click_users)
    
    trials = np.array([tips_click_users['user_id'].nunique(),
                       dont_tips_click_users['user_id'].nunique()])
    print('trials:', trials)
    
    successes = np.array([click_in_contacts_show['user_id'].nunique(),
                          dont_click_in_contacts_show['user_id'].nunique()])
    print('successes:', successes)

    print()
    
    # пропорция успехов в первой группе:
    p1 = successes[0] / trials[0]
    print('Конверсия в первой группе равна:', '{0:.2%}'.format(p1))
    
    # пропорция успехов во второй группе:
    p2 = successes[1] / trials[1]
    print('Конверсия во второй группе равна:', '{0:.2%}'.format(p2))
    print()
    # пропорция успехов в комбинированном датасете:
    p_combined = (successes[0] + successes[1]) / (trials[0] + trials[1])
    print('p_combined:', p_combined)
    # разница пропорций в датасетах
    difference = p1 - p2 
    print('difference:', difference)
    assert((p_combined * (1 - p_combined) * (1 / trials[0] + 1 / trials[1])) > 0)
    
    # считаем статистику в ст.отклонениях стандартного нормального распределения
    z_value = difference / mth.sqrt(p_combined * (1 - p_combined) * (1 / trials[0] + 1 / trials[1]))
    print('z_value:', z_value)
    # задаем стандартное нормальное распределение (среднее 0, ст.отклонение 1)
    distr = st.norm(0, 1)

    p_value = (1 - distr.cdf(abs(z_value))) * 2
    
    print()
    print('p-значение: ', p_value)
    print()
    
    if p_value < alpha:
        print('p-value < alpha, поэтому нужно отвергнуть нулевую гипотезу.')
        print('Есть основания говорить, что между долями есть существенные различия.')
    else:
        print('p-value > alpha, поэтому нельзя отвергнуть нулевую гипотезу.') 
        print('Есть основания говорить, что между долями нет существенных различий.')
In [98]:
z_test(0.05)
'group_1:'
session_start event_name user_id dt source
28 2019-10-07 00:00:02.245341 tips_show cf7eda61-9349-469f-ac27-e5b6f5ec475c 2019-10-07 yandex
29 2019-10-07 00:00:56.319813 advert_open cf7eda61-9349-469f-ac27-e5b6f5ec475c 2019-10-07 yandex
30 2019-10-07 00:01:19.993624 tips_show cf7eda61-9349-469f-ac27-e5b6f5ec475c 2019-10-07 yandex
31 2019-10-07 00:01:49.732803 advert_open cf7eda61-9349-469f-ac27-e5b6f5ec475c 2019-10-07 yandex
32 2019-10-07 00:02:06.225301 tips_show cf7eda61-9349-469f-ac27-e5b6f5ec475c 2019-10-07 yandex
... ... ... ... ... ...
73424 2019-11-03 16:51:57.923473 tips_show 3c14b914-b869-47d3-876b-0e83f9016e28 2019-11-03 yandex
73425 2019-11-03 16:54:31.870127 tips_show 3c14b914-b869-47d3-876b-0e83f9016e28 2019-11-03 yandex
73426 2019-11-03 16:56:01.603678 tips_click 3c14b914-b869-47d3-876b-0e83f9016e28 2019-11-03 yandex
73427 2019-11-03 16:56:08.706679 tips_show 3c14b914-b869-47d3-876b-0e83f9016e28 2019-11-03 yandex
73428 2019-11-03 16:57:04.230274 tips_show 3c14b914-b869-47d3-876b-0e83f9016e28 2019-11-03 yandex

12262 rows × 5 columns

'group_2:'
session_start event_name user_id dt source
0 2019-10-07 00:00:00.431357 advert_open 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
1 2019-10-07 00:00:01.236320 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
2 2019-10-07 00:00:07.039334 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
3 2019-10-07 00:01:27.770232 advert_open 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
4 2019-10-07 00:01:34.804591 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other
... ... ... ... ... ...
74192 2019-11-03 23:46:47.068179 map d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google
74193 2019-11-03 23:46:58.914787 advert_open d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google
74194 2019-11-03 23:47:01.232230 tips_show d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google
74195 2019-11-03 23:47:47.475102 advert_open d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google
74196 2019-11-03 23:47:50.087645 tips_show d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google

44065 rows × 5 columns

trials: [ 297 2504]
successes: [ 91 425]

Конверсия в первой группе равна: 30.64%
Конверсия во второй группе равна: 16.97%

p_combined: 0.18421992145662264
difference: 0.13666887189251406
z_value: 5.744518044318512

p-значение:  9.218316554537864e-09

p-value < alpha, поэтому нужно отвергнуть нулевую гипотезу.
Есть основания говорить, что между долями есть существенные различия.

Мы провели исследование о равенстве долей двух групп пользователей. Пользователей разбили на 2 группы:

  • На тех, кто совершил действие (event_name == tips_show), а следующим действием было (==) tips_click;
  • На тех, кто совершил действие (event_name == tips_show), а следующее действие было не (!=) tips_click.

В тесте между долями были существенные различия, поэтому пришлось отвергнуть нулевую гипотезу. Конверсия в первой группе равна 30,64%, а во второй группе 16,97%. Это говорит о том, что конверсии пользователей в 2х группах - существенно различаются.

Гипотеза о различии медианной длительности сессии между двумя группами (group_1 - достигшими целевого действия (event_name == contacts_show) и group_2 - не достигшими целевого действия (event_name != contacts_show))¶

Для выполнения данной задачи нужно добавить в датасет новую колонку со временем сессии.

In [99]:
df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 74197 entries, 0 to 74196
Data columns (total 5 columns):
 #   Column         Non-Null Count  Dtype         
---  ------         --------------  -----         
 0   session_start  74197 non-null  datetime64[ns]
 1   event_name     74197 non-null  object        
 2   user_id        74197 non-null  object        
 3   dt             74197 non-null  object        
 4   source         74197 non-null  object        
dtypes: datetime64[ns](1), object(4)
memory usage: 2.8+ MB
In [100]:
df = df.merge(
    final_df[['session_start', 'event_name', 'user_id', 'session_id']],
    on=['session_start', 'event_name', 'user_id'],
    how='left'
)
df
Out[100]:
session_start event_name user_id dt source session_id
0 2019-10-07 00:00:00.431357 advert_open 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other 2
1 2019-10-07 00:00:01.236320 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other 2
2 2019-10-07 00:00:07.039334 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other 2
3 2019-10-07 00:01:27.770232 advert_open 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other 2
4 2019-10-07 00:01:34.804591 tips_show 020292ab-89bc-4156-9acf-68bc2783f894 2019-10-07 other 2
... ... ... ... ... ... ...
74192 2019-11-03 23:46:47.068179 map d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google 7778
74193 2019-11-03 23:46:58.914787 advert_open d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google 7778
74194 2019-11-03 23:47:01.232230 tips_show d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google 7778
74195 2019-11-03 23:47:47.475102 advert_open d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google 7778
74196 2019-11-03 23:47:50.087645 tips_show d157bffc-264d-4464-8220-1cc0c42f43a9 2019-11-03 google 7778

74197 rows × 6 columns

In [101]:
# для удобства отсортируем датасет по user_id, тогда события каждого пользователя будут идти последовательно
df = df.sort_values(['user_id', 'session_start'])

# в колонке diff для каждого события отдельного пользователя посчитаем разницу между временем посещения страницы
# и временем посещения предыдущей страницы
df['diff'] = df.groupby('user_id')['session_start'].diff(1)

# вычислим время, проведенное на странице, руководствуясь временем посещения следующей страницы
# для этого сначала считаем разницу между предыдущей и следующей страницей внутри сессии
df['time_on_page'] = df.groupby(['session_id'])['session_start'].diff(1)

# сдвинем значение столбца time_on_page на одну строку вверх внутри отдельно взятой сессии
df['time_on_page'] = df.groupby(['session_id'])['time_on_page'].shift(-1)

# преобразуем time_on_page в секунды
df['time_on_page'] = df['time_on_page'] / np.timedelta64(1, 's')

df
Out[101]:
session_start event_name user_id dt source session_id diff time_on_page
2171 2019-10-07 13:39:45.989359 tips_show 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 other 0 NaT 45.06
2172 2019-10-07 13:40:31.052909 tips_show 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 other 0 0 days 00:00:45.063550 34.67
2173 2019-10-07 13:41:05.722489 tips_show 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 other 0 0 days 00:00:34.669580 135.01
2174 2019-10-07 13:43:20.735461 tips_show 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 other 0 0 days 00:02:15.012972 130.18
2175 2019-10-07 13:45:30.917502 tips_show 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 2019-10-07 other 0 0 days 00:02:10.182041 12.29
... ... ... ... ... ... ... ... ...
19048 2019-11-03 15:51:23.959572 tips_show fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 google 7816 0 days 00:00:27.886483 33.94
19049 2019-11-03 15:51:57.899997 contacts_show fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 google 7816 0 days 00:00:33.940425 943.03
19050 2019-11-03 16:07:40.932077 tips_show fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 google 7816 0 days 00:15:43.032080 37.27
19051 2019-11-03 16:08:18.202734 tips_show fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 google 7816 0 days 00:00:37.270657 7.19
19052 2019-11-03 16:08:25.388712 tips_show fffb9e79-b927-4dbb-9b48-7fd09b23a62b 2019-11-03 google 7816 0 days 00:00:07.185978 NaN

74197 rows × 8 columns

In [102]:
df_1 = df.groupby(['user_id', 'session_id']).agg({'time_on_page': 'sum'}).reset_index()
df_1
Out[102]:
user_id session_id time_on_page
0 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 0 595.73
1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 408 507.39
2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 3517 899.27
3 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 3843 758.17
4 00157779-810c-4498-9e05-a1e9e3cedf93 2972 1520.79
... ... ... ...
7812 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 6374 8052.82
7813 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 6687 40562.52
7814 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7291 22.31
7815 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7531 65641.52
7816 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7816 5729.43

7817 rows × 3 columns

In [103]:
df_2 = df.groupby(['user_id', 'session_id', 'event_name']).agg({'time_on_page': 'median'}).reset_index()
df_2
Out[103]:
user_id session_id event_name time_on_page
0 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 0 tips_show 54.82
1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 408 map 196.58
2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 408 tips_show 114.23
3 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 3517 map 48.97
4 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 3517 tips_show 46.39
... ... ... ... ...
14769 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7291 tips_show 22.31
14770 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7531 contacts_show 2106.21
14771 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7531 tips_show 568.45
14772 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7816 contacts_show 137.89
14773 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7816 tips_show 33.94

14774 rows × 4 columns

In [104]:
df_2['show_is_true'] = df_2['event_name'] == 'contacts_show'
df_2
Out[104]:
user_id session_id event_name time_on_page show_is_true
0 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 0 tips_show 54.82 False
1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 408 map 196.58 False
2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 408 tips_show 114.23 False
3 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 3517 map 48.97 False
4 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 3517 tips_show 46.39 False
... ... ... ... ... ...
14769 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7291 tips_show 22.31 False
14770 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7531 contacts_show 2106.21 True
14771 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7531 tips_show 568.45 False
14772 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7816 contacts_show 137.89 True
14773 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7816 tips_show 33.94 False

14774 rows × 5 columns

За длительность сессии посчитал сумму времени событий в одной сессии, т.е. сколько времени пользователь был активен внутри одной сессии. Кажется данный метод подсчета показывает более точное время затраченное пользователем внутри одной сессии.

Проверим пропуски в колонке time on page в первом датасете:

In [105]:
df_1['isnull'] = df_1['time_on_page'].isnull()
df_1[df_1['isnull'] == True].count()
Out[105]:
user_id         0
session_id      0
time_on_page    0
isnull          0
dtype: int64

Пропусков нет.

Далее посчитаем медианные длительности сессий для двух групп:

  1. Группу пользователей (user_id), которые смотрели контакты (event_name == contacts_show)
  2. Группу пользователей (user_id), которые не смотрели контакты (event_name != contacts_show)
In [106]:
df_1
Out[106]:
user_id session_id time_on_page isnull
0 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 0 595.73 False
1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 408 507.39 False
2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 3517 899.27 False
3 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 3843 758.17 False
4 00157779-810c-4498-9e05-a1e9e3cedf93 2972 1520.79 False
... ... ... ... ...
7812 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 6374 8052.82 False
7813 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 6687 40562.52 False
7814 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7291 22.31 False
7815 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7531 65641.52 False
7816 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7816 5729.43 False

7817 rows × 4 columns

In [107]:
df_2
Out[107]:
user_id session_id event_name time_on_page show_is_true
0 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 0 tips_show 54.82 False
1 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 408 map 196.58 False
2 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 408 tips_show 114.23 False
3 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 3517 map 48.97 False
4 0001b1d5-b74a-4cbf-aeb0-7df5947bf349 3517 tips_show 46.39 False
... ... ... ... ... ...
14769 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7291 tips_show 22.31 False
14770 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7531 contacts_show 2106.21 True
14771 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7531 tips_show 568.45 False
14772 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7816 contacts_show 137.89 True
14773 fffb9e79-b927-4dbb-9b48-7fd09b23a62b 7816 tips_show 33.94 False

14774 rows × 5 columns

In [108]:
all_sessions = df_1
print('Количество всех сессий:', all_sessions['session_id'].nunique())

show_sessions = df_2.query('event_name == "contacts_show"')
print('Количество сессий, в которых смотрели контакты:', show_sessions['session_id'].nunique())
show_sessions_list = show_sessions['session_id'].unique().tolist()
show_sessions = df_1.query('session_id in @show_sessions_list')

dont_show_sessions = df_1.query('session_id not in @show_sessions_list')
print('Количество сессий, в которых не смотрели контакты:', dont_show_sessions['session_id'].nunique())
Количество всех сессий: 7817
Количество сессий, в которых смотрели контакты: 1444
Количество сессий, в которых не смотрели контакты: 6373

Теперь можно приступать к проверке гипотез.

Напишем нулевую и альтернативную гипотезы.

H0: Нет различий в медианной длительности сессии между группами (group_1 - достигшими целевого действия (event_name == contacts_show) и group_2 - не достигшими целевого действия (event_name != contacts_show));

H1: Есть различия в медианной длительности сессии между группами (group_1 - достигшими целевого действия (event_name == contacts_show) и group_2 - не достигшими целевого действия(event_name != contacts_show)).

Для сравнения медианной длительности используем непараметрический тест Уилкоксона-Манна-Уитни.

In [109]:
show_sessions = show_sessions['time_on_page']
dont_show_sessions = dont_show_sessions['time_on_page']

alpha = 0.05

results = st.mannwhitneyu(show_sessions, dont_show_sessions)

print('p-значение:', results.pvalue)
print('Медианная длительность сессии в группе достигших целевого действия:', show_sessions.median())
print('Медианная длительность сессии в группе не достигших целевого действия:', dont_show_sessions.median())

if results.pvalue < alpha:
    print('Отвергаем нулевую гипотезу: разница статистически значима')
else:
    print('Не получилось отвергнуть нулевую гипотезу, вывод о различии сделать нельзя')
p-значение: 1.4416460813065889e-74
Медианная длительность сессии в группе достигших целевого действия: 1992.762575
Медианная длительность сессии в группе не достигших целевого действия: 599.80295
Отвергаем нулевую гипотезу: разница статистически значима

Мы провели исследование о равенстве медианной длительности сессии между двумя группами пользователей:

  1. Группу сессий (session_id), в которых пользователь хоть раз смотрел контакты (event_name == contacts_show);
  2. Группу сессий (session_id), в которых пользователь не смотрел контакты (event_name != contacts_show).

Медианная длительность сессий в группе достигших целевого действия равна 1992 секундам, а во второй группе не достигших целевого действия - 599 секунд. Медианная длительность пользователей в 2х группах различаются существенно, нулевую гипотезу нужно отвергнуть.

Это говорит о том, что пользователи достигающие целевых действий пользуются приложением в 3.3 раза дольше, а также совершают в 2 раза больше действий чем пользователи нецелевой группы.

Общий вывод и рекомендации¶

Для достижения целей данного исследования мной были соверешены следующие действия:

  1. Загрузка данных: загрузка данных и изучение общей информации.
  • Импорт библиотек, настройка функций, отображений и загрузка датасетов
  • Изучение общей информации
  1. Предобработка данных: приведение названия столбцов к нижнему регистру, проверка пропусков, приведение данных к нужному типу.
  • Переименование названий столбцов
  • Приведение данных в нужный тип
  • Объединение действий пользователей
  • Промежуточный вывод
  1. Создание функций для расчетов: расписывание функций для создания данных.
  • Функция для создания пользовательских профилей - get_profiles()
  • Функция для расчёта удержания - get_retention()
  1. Исследовательский анализ данных: описание и визуализирование общей информации о пользователях.
  • Профили пользователей
  • Считаем Retention Rate
  • Визуализация когортного анализа
  1. Основные вопросы исследования:

    5.1. Анализ связь целевого события — просмотра контактов — и других действий пользователей.

    • В разрезе сессий отобрать сценарии\паттерны, которые приводят к просмотру контактов;
    • Построить воронки по основным сценариям в разрезе уникальных пользователей.

      5.2. Оценка действий которые совершают те пользователи, которые просматривают контакты.

      5.2.1 Рассчет относительной частоты событий в разрезе двух групп пользователей:

      • группа пользователей, которые смотрели контакты contacts_show;
      • группа пользователей, которые не смотрели контакты contacts_show
  2. Проверка статистических гипотез

  • Гипотеза о различии конверсии в просмотры контактов (целевое действие) между двумя группами (group_1 - tips_show, tips_click и group_2 - tips_show)
  • Гипотеза о различии медианной длительности сессии между двумя группами (group_1 - достигшими целевого действия (event_name == contacts_show) и group_2 - не достигшими целевого действия (event_name != contacts_show))
  1. Общий вывод и рекомендации: резюмирование полученных результатов, формулировка ключевых выводов и рекомендаций.

С помощью данного исследования я стремился дать всестороннний анализ пользователей мобильного приложения "Ненужные вещи" для дальнейшего развития и планирования расходов компании.

Cамыми популярными оказались сценарии:

  • (map, contacts_show)
  • (search, contacts_show)
  • (photos_show, contacts_show)
  • (search, photos_show, contacts_show)

Сценарий (map, contacts_show):

CR от открытия карты объявления к целевому действию - 20%.

RR для действия map от общего количества уникальных пользователей составляет 40,6%.\ RR для действия contacts_show от общего количества пользователей составляет 8,06%.

Сценарий (search, contacts_show):

CR от поиска объявления к целевому действию - 23%.

RR для действия searсh от общего количества уникальных пользователей составляет 38,81% - это значит, что 38,81% от всех пользователей занимались ручным поиском объявлений.\ RR для действия contacts_show от общего количества пользователей составляет 8,78%.

Сценарий (photos_show, contacts_show):

CR от просмотревших фото объявления к целевому действию - 31%.

RR для действия photos_show от общего количества уникальных пользователей составляет 25,51% - это значит, что 25,5% от всех пользователей просмотрели фото в объявлении.\ RR для действия contacts_show от общего количества пользователей составляет 7,9%.

Сценарий (search, photos_show, contacts_show):

CR от search к photos_show составляет 66%, от photos_show к целевому действию - 31%. Очень достойные показатели, это значит, что если пользователь посмотрел фото объявление после поиска, то он с вероятностью 31% добавит контакты продавца. Нужно каким-то образом подстегивать людей на данные действия. Может поможет геймификация?

RR для действия search от общего количества уникальных пользователей составляет 38,81%. RR для действия photos_show от общего количества пользователей составляет 25,51%. RR для действия contacts_show от общего количества пользователей составляет 7,9%.

Также хочется отдельно отметить следующий важный сценарий для олицетворения рекомендательной системы в приложении:

Сценарий (tips_show, tips_click, contacts_show):

Conversion от tips_show к tips_click составляет 10,6%, от tips_click к целевому действию - 30,6%. Это значит, что пользователь, который прошёл по рекомендации - с вероятностью 30% добавит контакты продавца. По самой рекомендательной системе есть ещё куда расти, но проблема чуть в другом: люди не пользуются рекомендациями. И именно из-за этой проблемы лишь 2,9% от tips_show доходят до целевого действия.

Retention для действия tips_show от общего количества уникальных пользователей составляет 65,25% - это значит, что 65% от всех пользователей увидели рекомендованное объявление, данное действие является контролируемой и не зависит от поведения пользователей. Retention для действия tips_click от общего количества пользователей составляет 6,92%. Retention для действия contacts_show от общего количества пользователей составляет 2,12%.

Разница в совершенных действиях и источниках между группами незначительная. Нельзя с уверенностью сказать, что группа, которая достигает целевого действия - достигает именно из-за 'X' действия. Но пользователи, которые просмотрели контакты - более активны. Больше действий в photos_show и map, чем у тех, кто не смотрел контакты. Но у достигших целевого действия - меньше открытий объявлений (advert_open).

Важно ещё также отметить, что ровно с третьей недели - увеличивается активность группы, совершивших целевое действие.

В данном анализе была проверена гипотеза о различии конверсии в просмотры контактов. Для этого пользователей разбили на 2 группы:

  • Группа пользователей прошедшие через сценарий (tips_show, tips_click, contacts_show);
  • Группа пользователей прошедшие через сценарий (tips_show, contacts_show).

В тесте между долями были существенные различия, поэтому пришлось отвергнуть нулевую гипотезу. Конверсия в первой группе равна 30,64%, а во второй группе 16,97%. Это говорит о том, что конверсии пользователей в 2х группах - различаются существенно.

Также мы провели исследование о равенстве медианной длительности сессии между двумя группами пользователей:

  1. Группу сессий (session_id), в которых пользователь хоть раз смотрел контакты (event_name == contacts_show);
  2. Группу сессий (session_id), в которых пользователь не смотрел контакты (event_name != contacts_show).

Медианная длительность сессий в группе достигших целевого действия равна 1992 секундам, а во второй группе не достигших целевого действия - 599 секунд. Медианная длительность пользователей в 2х группах различаются существенно, нулевую гипотезу пришлось отвергнуть.

Это говорит о том, что пользователи достигающие целевых действий пользуются приложением в 3.3 раза дольше и совершают больше действий чем пользователи нецелевой группы. Это может работать и в обратную сторону: чем более активным является пользователь (по длительности сессий и количеству действий), тем больше вероятность, что он достигнет целевого действия. Таким образом, одним из рекомендаций будет увеличение активности пользователей, для повышения вероятности достижения целевого действия.

Мои рекомендации:

Для увеличения активности пользователей - можно сделать сделать геймификацию:\ Платить условные баллы (которые можно потратить на рекламу своего объявления и т.п.) за определенные действия, которые интересены для бизнеса (такие как: произвести 10 поисков в день, добавить 5 товаров в избранное или заходить неделю подряд). Данный способ поможет поднять активность пользователей.

Также для увеличения активности пользователей можно расширить функционал приложения добавляя новые возможности: возможность выбора транспортной компанией в случае успешной продажи; возможность добавления (также их привлечение) компаний по продажам вещей, например, секондхенды или оффлайн магазины и т.д.

Для улучшения работы приложения не в количестве аудитории, а в качестве стоит подумать над новой рекомендательной системой или её улучшением. Довольно большое количество событий tips_show было показано пользователям, а конверсия от tips_show к tips_click составляет всего лишь 10,6%. Возможно пользователи не видят этих рекомендаций, может стоит увеличить их в размере или поставить в более видное место, может они просто не хотят переходить по ним (в последнем случае поможет геймификация). Стоит разобраться с этой проблемой.\ Сама рекомендательная система показывает 30% конверсию от tips_click к целевому действию, над этими цифрами тоже можно поработать улучшив саму рекомендательную систему.

Но ещё также нужно обеспечивать постоянный поток новых пользователей с помощью поиска новых каналов привлечения (реклама в разных соц сетях, блогеров, может быть ТВ или шоу).

Для более подробной аналитики не помешало бы иметь информацию в датасете из каких каналов были привлечены новые пользователи, чтобы можно было отслеживать с каких каналов лучшее удержание пользователей, стоимость привлечения одного пользователя, расчеты LTV, ROI, CAC и в целом для прогнозирования маркетинговых расходов на будущее.